From d569972687b3c98fe6962f7557b1e6a06524df3b Mon Sep 17 00:00:00 2001 From: mickychetta Date: Thu, 29 Sep 2022 19:13:14 +0000 Subject: [PATCH 1/9] created README --- .../aws-lambda-opensearch/README.md | 164 ++++++++++++++++++ .../aws-lambda-opensearch/architecture.png | Bin 0 -> 66516 bytes 2 files changed, 164 insertions(+) create mode 100755 source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/README.md create mode 100644 source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/architecture.png diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/README.md b/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/README.md new file mode 100755 index 000000000..72d835556 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/README.md @@ -0,0 +1,164 @@ +# aws-lambda-opensearch module + + +--- + +![Stability: Experimental](https://img.shields.io/badge/stability-Experimental-important.svg?style=for-the-badge) + +> All classes are under active development and subject to non-backward compatible changes or removal in any +> future version. These are not subject to the [Semantic Versioning](https://semver.org/) model. +> This means that while you may use them, you may need to update your source code when upgrading to a newer version of this package. + +--- + + +| **Reference Documentation**:| https://docs.aws.amazon.com/solutions/latest/constructs/| +|:-------------|:-------------| +
+ +| **Language** | **Package** | +|:-------------|-----------------| +|![Python Logo](https://docs.aws.amazon.com/cdk/api/latest/img/python32.png) Python|`aws_solutions_constructs.aws_lambda_opensearch`| +|![Typescript Logo](https://docs.aws.amazon.com/cdk/api/latest/img/typescript32.png) Typescript|`@aws-solutions-constructs/aws-lambda-opensearch`| +|![Java Logo](https://docs.aws.amazon.com/cdk/api/latest/img/java32.png) Java|`software.amazon.awsconstructs.services.lambdaopensearch`| + +## Overview +This AWS Solutions Construct implements the AWS Lambda function and Amazon OpenSearch Service with the least privileged permissions. + +**Some cluster configurations (e.g VPC access) require the existence of the `AWSServiceRoleForAmazonOpenSearchService` Service-Linked Role in your account.** + +**You will need to create the service-linked role using the AWS CLI once in any account using this construct (it may have already been executed to support other stacks):** +``` +aws iam create-service-linked-role --aws-service-name es.amazonaws.com +``` + +Here is a minimal deployable pattern definition: + +Typescript +``` typescript +import { Construct } from 'constructs'; +import { Stack, StackProps, Aws } from 'aws-cdk-lib'; +import { LambdaToOpenSearch } from '@aws-solutions-constructs/aws-lambda-opensearch'; +import * as lambda from "aws-cdk-lib/aws-lambda"; + +const lambdaProps: lambda.FunctionProps = { + code: lambda.Code.fromAsset(`lambda`), + runtime: lambda.Runtime.NODEJS_14_X, + handler: 'index.handler' +}; + +new LambdaToOpenSearch(this, 'sample', { + lambdaFunctionProps: lambdaProps, + domainName: 'testdomain', + // TODO: Ensure the Cognito domain name is globally unique + cognitoDomainName: 'globallyuniquedomain' + Aws.ACCOUNT_ID +}); +``` + +Python +```python +from aws_solutions_constructs.aws_lambda_opensearch import LambdaToOpenSearch +from aws_cdk import ( + aws_lambda as _lambda, + Aws, + Stack +) +from constructs import Construct + +lambda_props = _lambda.FunctionProps( + code=_lambda.Code.from_asset('lambda'), + runtime=_lambda.Runtime.PYTHON_3_9, + handler='index.handler' +) + +LambdaToOpenSearch(self, 'sample', + lambda_function_props=lambda_props, + domain_name='testdomain', + # TODO: Ensure the Cognito domain name is globally unique + cognito_domain_name='globallyuniquedomain' + Aws.ACCOUNT_ID + ) +``` + +Java +``` java +import software.constructs.Construct; + +import software.amazon.awscdk.Stack; +import software.amazon.awscdk.StackProps; +import software.amazon.awscdk.Aws; +import software.amazon.awscdk.services.lambda.*; +import software.amazon.awscdk.services.lambda.Runtime; +import software.amazon.awsconstructs.services.lambdaopensearch.*; + +new LambdaToOpenSearch(this, "sample", + new LambdaToOpenSearchProps.Builder() + .lambdaFunctionProps(new FunctionProps.Builder() + .runtime(Runtime.NODEJS_14_X) + .code(Code.fromAsset("lambda")) + .handler("index.handler") + .build()) + .domainName("testdomain") + // TODO: Ensure the Cognito domain name is globally unique + .cognitoDomainName("globallyuniquedomain" + Aws.ACCOUNT_ID) + .build()); +``` +## Pattern Construct Props + +| **Name** | **Type** | **Description** | +|:-------------|:----------------|-----------------| +|existingLambdaObj?|[`lambda.Function`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_lambda.Function.html)|Existing instance of Lambda Function object, providing both this and `lambdaFunctionProps` will cause an error.| +|lambdaFunctionProps?|[`lambda.FunctionProps`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_lambda.FunctionProps.html)|User provided props to override the default props for the Lambda function.| +|domainProps?|[`opensearchservice.CfnDomainProps`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_opensearchservice.CfnDomainProps.html)|Optional user provided props to override the default props for the OpenSearch Service.| +|domainName|`string`|Domain name for the Cognito and the OpenSearch Service.| +|cognitoDomainName?|`string`|Optional Cognito domain name, if provided it will be used for Cognito domain, and `domainName` will be used for the OpenSearch domain.| +|createCloudWatchAlarms?|`boolean`|Whether to create the recommended CloudWatch alarms.| +|domainEndpointEnvironmentVariableName?|`string`|Optional name for the OpenSearch domain endpoint environment variable set for the Lambda function.| +|existingVpc?|[`ec2.IVpc`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ec2.IVpc.html)|An optional, existing VPC into which this pattern should be deployed. When deployed in a VPC, the Lambda function will use ENIs in the VPC to access network resources. If an existing VPC is provided, the `deployVpc` property cannot be `true`. This uses `ec2.IVpc` to allow clients to supply VPCs that exist outside the stack using the [`ec2.Vpc.fromLookup()`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ec2.Vpc.html#static-fromwbrlookupscope-id-options) method.| +|vpcProps?|[`ec2.VpcProps`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ec2.VpcProps.html)|Optional user provided properties to override the default properties for the new VPC. `enableDnsHostnames`, `enableDnsSupport`, `natGateways` and `subnetConfiguration` are set by the pattern, so any values for those properties supplied here will be overridden. If `deployVpc` is not `true` then this property will be ignored.| +|deployVpc?|`boolean`|Whether to create a new VPC based on `vpcProps` into which to deploy this pattern. Setting this to true will deploy the minimal, most private VPC to run the pattern:If this property is `true` then `existingVpc` cannot be specified. Defaults to `false`.| + +## Pattern Properties + +| **Name** | **Type** | **Description** | +|:-------------|:----------------|-----------------| +|lambdaFunction|[`lambda.Function`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_lambda.Function.html)|Returns an instance of `lambda.Function` created by the construct| +|userPool|[`cognito.UserPool`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_cognito.UserPool.html)|Returns an instance of `cognito.UserPool` created by the construct| +|userPoolClient|[`cognito.UserPoolClient`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_cognito.UserPoolClient.html)|Returns an instance of `cognito.UserPoolClient` created by the construct| +|identityPool|[`cognito.CfnIdentityPool`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_cognito.CfnIdentityPool.html)|Returns an instance of `cognito.CfnIdentityPool` created by the construct| +|opensearchDomain|[`opensearchservice.CfnDomain`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_opensearchservice.CfnDomain.html)|Returns an instance of `opensearch.CfnDomain` created by the construct| +|opensearchDomain|[`iam.Role`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_iam.Role.html)|Returns an instance of `iam.Role` created by the construct for opensearch.CfnDomain| +|cloudwatchAlarms?|[`cloudwatch.Alarm[]`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_cloudwatch.Alarm.html)|Returns a list of `cloudwatch.Alarm` created by the construct| +|vpc?|[`ec2.IVpc`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ec2.IVpc.html)|Returns an interface on the VPC used by the pattern (if any). This may be a VPC created by the pattern or the VPC supplied to the pattern constructor.| + +## Lambda Function + +This pattern requires a lambda function that can post data into the OpenSearch. A sample function is provided [here](https://github.com/awslabs/aws-solutions-constructs/blob/master/source/patterns/%40aws-solutions-constructs/aws-lambda-opensearch/test/lambda/index.js). + +## Default settings + +Out of the box implementation of the Construct without any override will set the following defaults: + +### AWS Lambda Function +* Configure limited privilege access IAM role for Lambda function +* Enable reusing connections with Keep-Alive for Node.js Lambda function +* Enable X-Ray Tracing +* Set Environment Variables + * (default) DOMAIN_ENDPOINT + * AWS_NODEJS_CONNECTION_REUSE_ENABLED (for Node 10.x and higher functions) + +### Amazon Cognito +* Set password policy for User Pools +* Enforce the advanced security mode for User Pools + +### Amazon OpenSearch Service +* Deploy best practices CloudWatch Alarms for the OpenSearch Domain +* Secure the Kibana dashboard access with Cognito User Pools +* Enable server-side encryption for OpenSearch Domain using AWS managed KMS Key +* Enable node-to-node encryption for OpenSearch Domain +* Configure the cluster for the Amazon OpenSearch domain + +## Architecture +![Architecture Diagram](architecture.png) + +*** +© Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/architecture.png b/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..58e38198af9704a3e6e2067a346a0cd67d22066d GIT binary patch literal 66516 zcmeFYS6ov~7dNUAq=_^I6zNT+caYwD?>+RE&${Xq{J>L=;&(~=;p=i;N|PhYX`MMczFWPP~Zok7u3tq&DqV#@t>Z8{DOi!{1QBZ z5=H_N>_YPV;=oNnlvhGX%;KN^b}mky{~K1@3*iQV*s%+$i16|QL*3Q3b8z$Y_wsXP z7g7Met9$x8L4Z%78Mx~j0yi_@zW~3jfQYTwJ>XH^+Z*C!>SV9!=6^AYkQlF!DA0UY zQ_VUsux{;tjv5H-X<+`ycC{oK4f|LGri4SFqPM!Cm|l?kKjV1X0W|%VwH_cK9)W)b5i^1KyG!_a zDeDOXEea;?TB_=9M()B;gq<-!teuZffR>RP%n>5&YYtboSAqL!d005>`6>x&ItVJN zd5ig4Xb1)fI++Ogx~dwuyFkq#P;FgL1x+0Zafqgoww_mjvN%M{MMK2T$y3izQC$JS zFJUI`?xg9Z5-1PMsATG`p$UPwTj(1pxSL9N^ZVoC%>LTtcu39id0}(qfUk!6zKQ&-PeP>a1KZJ#kg^Q`U zfUc>Bs42gyiHEVFpJ|YWuz;|xfr_%0iLY6pk+G?myq%DuH=JKc&rZ)l%R|gSUkeTm zG;@Fjc?bsq>?`=_0C(W>5j0c~G6EjNo%IA=gv?akV2ThSdj&^dHE$nf1qCg*2}HpQ zAtdkY0#KlGBnUo)fMyh*41)H7#sTunVYEFoBL?# z18;a}sLM;}2nQOu!rU!9g>~gMT_F%hO*cTb3jE$Co_=aR0vd)IW(IIY5l;g<6E{Jq zr;vrRn23w0o4JmWxS63BR9wJRP1)4c%h=G{z|JrLZttt&s3M_n>*YI;xb2pNQ1&H$V zJG+X&oC2J@MFU}KhJHpm2#A{jG(cU|%-#VaE+(pD=%=jZFX5%9re_og_xHBYG~jpk z7chpoikUeFD#4WW1I=Jw0YWZ9c1~(;PMUhY#zH>o;;Q;~Fm*9QCwU_YZ8KpZh^C!` ziJG^FilK23!dJx9L{r4vNKxF#K_4&|1q&C1nwFmq!q4B-TtQFN%)`|Vp{8#q;iWCE zvj*l;o9M+zj;PeMO7`hoAs))Ri|k_VUq&1!~Ic1Hcwk z6%+@&hKia!M99ln4I*UjYpSfTrK=1P3sgj?n290;Bz$$D`YK}X9tP&BYU1WlO&2?1 zV=W(LBP~y0M0+iJ2>~;xkD`-KLgDrhVP~kDnue#p9x$*d;D%Hr%te&^ zv;tMtJpB}c#C>4`4(?7)dZJ=tstziCp6X`G;^r25CI)_{T5x3pK~V)=At4cQRb5vX z5d{emZ&5{WFJTF|r=GdLhoYi4FrK`Rg^Gr=orQ_LjL6kV6LXfZH`5SsRngRP73KF8 zftu(Es|mm~4NSZOHSEL?7S2LiZU`4)2Q#RkFYuy?YE(@5c z7~Ijp%l=|c0ZkDj4@YOUKykebS74zaAQ}Ml)Y29U5Of2avVy6Su9LdBwvM-%y||Mz zG{`#uE&(+Ad1(j%{R07ytpqr2;PH>&{_Vtp@Bdp6g_NJ>P(d$UV!EWRByR+_!DQmU zpjV!F!WG9sPKHOK?(GtB(~LEE!N*+EBU=8wg#?$oQg_8nSN`a4=~bZah^`TwJWqpLB`n`G2lFg}0>mu={s3 zt-m}X3O~Sf>mG$4HBMJvxAf%QB2zyNFTVT@PRVrnf0tU)C4AUZ{vM;7QG4&_OidpL zP)_E5E11<$NODqth^O}ZvLLxkj2RV7zJB`royER^&x-w{grJGq<( zvs}cKg^J)?#`1QHCt+{zr-)5ZYuI~OJcT@6znbQVVfjO<+)H3uuB>Zz?H0uDgkWPU zRe6?$TBrKhV(!(#&j|`IgZ@tbRqG+Bu{W0nj0-+JC^JXituA0Lf%a)6L8&HhOqBUo zRa&yif&RHf@a`fl$D8=2*)6UzUL*ha4R3h9yi-p3 zR`5CD0jO8^e{-|xT?O4hJJIG}qJuIsuSuUgG$n2qje{P|L%Llk$BR@aEOv6<9{nYQ zZIn9n%8rJ&l9}e{m0i=D#?uF~Oui4k9shUuZwkOGQPpkK)b^_y)Q!=2(C$-_?Z;{pnztrDc zo3OpTzr_B2L*7H2OA==5k~I&PS?p(c?;% ze_;XwCQ$NuR8jd?SNZasjtQ_-VqxJh4xiQ;D*BsdYN{H<_2%zW&le50Ebh{#x>%QYL9raz7FWLGy zMrH9*`zFjZt|%id`_HIvGXQy!Jn%fOh4Bj&x4y6+#|m91?o`pNr01v~EQipMkqdr# zu+?}RezLK;p5P$a^B+J_1MgP9eWQ?BaJVMk7%lgf<+aq{y|k555a^bICCy~P8G6E$ zkub9-BG%rB;J>I-d9o#)t(9ee4r*$`0i+&vRnRmQhy1n6!5$gW*T$f7w z%$_%;^fR{i+6;fS#S|e$qz7JBpR6nsrV-a*&hWRhPg|*?H@=qs70qx%i|l5t=mKK5 z&zM>cixe#1NX}Ud2DX^~eBD_TMsmcc!-78>vqBp|)5BS&%X3 zw3aW&RQ(HRCZHSs8%AIGY`4OM5BN@K8+7fI!cS1gT@upVPE!YQEgMf_3`pDPjl!Ry z$<+We>8I)p@Oi5ZZ+KG?Hro|YDxKN@#o4SmS=X&r^MLbOVYf(^nE&GV0yPif6`t4K z=8$*28kD5X=r`C%IjS;Y9XPJRuk~ZHc00FKC-{RI<6nz5xzIj~N3odJo&s2-!uy-& zohrn>;bIBD>Nvju*KSWkJGMhKbm_-%pST^HU58u*y-G~?U#Im{;UP$}JBg4uMelLV z7pk&wZ}x<71G%+#QaaY!75K>7e#syQ#2D!xo&s;CIx3@fs0IS=c?H_gq~YBeirdEe}Z)RrB$n{A2z;rbHMjf=IeKX>-H?Ilxn9!=`>j|_su zzJEi8hgU8WLkQH&9g`%z-g>5Xg_WcgM2p2y*k%3)#n%Cb{Kc!N|9GX}n%@E__hb3j zq5<=idkU!4FzQRmzqDS251@3vKY_$QKNtW`Et5sdMEMWselOoQ&>2_$ofK$#&H~JP z!>;Q7|1s~|)}=dtqnC?^h#P?A6cVNw|I5{dtH2js{_8P*KjZ9LP zTPC!#-e;ln3uiXBO8o7Y^)e>^5nWk4fJh=$qnqd+K2(#LU!O>5^Wol*Zr6)@h9%*u zEvB}i#_ww<++z`15t)RHB!9z8;Y$Dvd804Rq{QCI26h%UuChKi)Z}Mq-HndgEpbl_p>Fhq0b1*rc`;7VYqf zK}R#)aoiM!=+{$23FhROYMQ1IY8WgA+ORfm5S9HGJjtj6tw2`tFL=6JDbv!?w~ z%jtLBfit0X1>IDfIyVY4VQ*+kMu&9UJ&8Y}95CTrqRYnI$o1M8-XYG;I> z9nI&b+E7+I_ff7-*3nEts^br*Hp4itOYeQ7OfLBHFYw4S0a^Jw2k=oSgUdET2)Js7odx8|7i=t`@*^)zTwUfNlP8XftuX=*hnnec#lzA}F1 z*)1U{{m|viKt%UdG6rtmrmgm8MM#T(adh`$Wwd{$$i`(95syqb-9{UKI(DW6Orm$4<|X*To+~(_3XW>K8fzP5gQs z6|&VvAToK`zG9>KVA}HJXdoOLb<)ik-dm4R%kAs^%eoUQpu<`UDg~t}0=jnU%CS`f z7sTGLxi0lh)cKixZ<>G%ad!CGM>D&}H=9tb)VQYIej#EAcf!f|K-dz!1)kh?@w4@CE{1*0OX1_I0Tck_v|XLk#9GVj}j zp9XAq(b&Rk6&+Cr)4}gN#>zz`<9>wU*5{EO*M+cTjj?Y}A_y4nZhTy3$Zh@9&&pNN zR22y6rVrd}*V>ChQo-w!HX1i&_)OAc|Ab->@hrG5tRvyQ$2BYKYDMk6lPPO9`6eopxsN$I-`iFq~=a~g$8KxMH5)FJsK8YS#|>ath~wmPr&rn z|H9bC9BY_aH|4#ca-qcb6OL`;hSHktQ^@+M~oHPb2fY@^#_-cE)YC6r!iZ=23 zo~Lfy#?$yP&F!b^Z81+Pk!zhFw6m@FW382z`ZL!`*H%j_urkh(QPxT(7H;_&YL&PC z9shGSF#cIDRUNG{c){Ztdh<5p%cCCKv-z5eFvq1B)&z2~AKr>0Y z1%zHB%_jn*d@U7Jb#UYdsi^DWZ6xIOReE#~wA&{}gU%wGW8U!^kUbCS>u2JKh<{Fn z>5A9QW6hxtadkf4Tbb@>uPQ&72}d<7s1a2TIsVIyH{j>iql(R557)bQ>h|!7OuYXQ zdN74A=ZU9N-x%r% zT^!MKAi{8^l4~o(h$?JxN;t}pO=RgrWR5+4whlyqd785gku*l0F9g5PafVlrGbI`= zcgG(1s|A8+-qwDiM4R&5Q!-2;hfdrjwmd68f5AzfXbMl#<%!-ps@z+dp_#o=NOZv( zd1mWA0pAuc&Oq(!>j%8!XyAAW3ZcMZNO zjegv4>Dz^qbODXge~=T7EqEvv3WaBc7^E|DP7MtPdA?$IPz*w%nti`iJtJ9%;Nwkv zkDMY_NwQ{&_{8>&1)2YRY2fP>0tv^q2vFnCvDaP|W>rVA>6~iRoP>N*HM=GB(r9*+ z{kITGS1ZsMgE-1Zav*D2XErZ*rLCVH@rG0ZER-Z?gEx`YMvG}OtELCSd4D3S;BO&M zj0E!p>z-=>na9A)4-cF(8=mMu+K>LU-h-99*f9R}e#dLD4NeI?`qIA07DI{=xN%;Vj%>!3!U zDEz@~=!xtX=G`D9Mh=18mF^h74Wwrlc;(aK*A?&IGI(vNwpe@+pzX0rUBG}qE|#cH zKHq{b(lZ~W5#jZcA&?|~nYqjWt|*9xi6Q1@sM+m<2|Bi*&c!zJq!>=u!K!fSCUo2# z8q1mrv-hgmKeA|ZF`PR?y{TQgLZ80pQncmDlfeedFMWu{Q)_+MYK#lL>D@erU)Lf_ zlW0na#`3T-W)8R=1o~yZ$^SB@$AEd-%4=f*8IgIn-ZDTvz~A>S+qsFmCG@TB+&cHu zh@R_!w)HlNcpQMUq#zH9Jk^E!ic}icSN| zOukn?cA6dy3a0O#Mrn2(Eb!f9>u$o-|AEdG$0-zBi=e#FkVhlL4c(q~`fDOd7H>VJhmIO+U#=;P+q}hH}Ag zLOZD&CrE5p%B=Dd&1B#aqRc1?0o#_tRM(M9rrS)xpE9wq@Ul(=QaYXO; z+7wQEUoxB+V^yF5i|4*mYxK^P);#pMaq4VmIVdP#?2; zAu-pg+U zLhwPj9OZn53ZZlKl$*_~dxn4y^B>jGJ2CkJJnLorz8h~jI-Q_MS#E%G{p_g|BRqqT>M(|*p$pz4wqfB9~ zyWsHb7>~sL)El@}223<{EK&IJ1O4XxFgux9c?423RM~<*56WztgO)}QW?iaod53OB zOWsWO7?o;YEhK0hP9?dm3nJA9|CGs|q&H}|5{Vjn&>6L*gj5F)AU1vW)Z>Kk>DxtxkFF0#;z!4GGN7p3$; z?>zyyAEkCgS~q>@y{t=uYUNEVBOi2g*>toy4Tbp6oMTmjOP&?CXg5dT@lCu3(7@MFR zSH=g~s)h)%va|y8$3>;vQt0iv>|RXfcR_;NQs~s0zW2>}aksRvCe3L4pXnb^K0Sd_ zsSFqet3_8lz;`=>Iu6d>FDeR62Uv%xsqlWB+kE-fMTo4lwVc=YS+~~+DLIQhIhxEa z$)QZz^32XWBHWyV5F#bcmdOZIy%P+s2Mk2I)_C^VO_sCbP<+A9+ag0(zJHA}Zh3Be z2}iRzV#*ynJ9L!~t25v2#z!-{@rp!FMoKbMl?-;hP$V6wZhd~(ij)nZT&EAO9Q`>q zC0{e6JOzLsM=(GwM7hBNifEpBJmoHpCgZ)az-VD6WK$4wON**P13s{i&3#aC_0%o@ zeQ&8{GwEU*77uorsp6{mYSNr!N3?&{u;=q2N5g49mo$G^6&YU-s@xp^_&j51 zwa!C26R3bozBU&EbX$P;uifE|xn5_ZRi4qN(a7d3n#&UDY2I5AJ zcH`C1*zAoDs^djT11awkp{2F6=_NbaxnDot;epsRRwU7Ik zhaxk@{AyK!V(RsX9MpMzjOMEy8x5+)<}17^F`o@YA_bNnjoU8;&#=!=_(kS(GhpyV z_C1?*39<TKKtOSjdE_nWiGerR4HLFvSXvK`?<@#ct~$XO4Bev z*6O~qhG={_EZaX^>;pAgSLY66p$CWSuTeJ{D_XDw(u;A7=bPxB&N@C@6JlI-23h97 zc|nVdnB~O|OGFx;J6`S5mzT!Jo`ro!a4R?qniF|}tC`dC2P{5^`&P1YV6Kz8iAi2$ zN87tZSCT7x|_2&|ei z+9FT$MNp;MzerY1sG7bbm9=k@!Sc@qU+;XZEnSkt4U`VwxDqKq6LPDt#Qj!tztYQ_ zdEOmFm`C0f0=cJcVSHOZG|BmF5-2%TRsWm?aI8YDcJZpx2v#NoF$J>90O0-(;R|bU zI-WB%8uIQX;FqAp7}qA^)ZqTEE~}rvf{?-c#d9Ld^YeVZFXB!ou+q98)`QEvn&Q4r z1|->k53%M2|95d(M5V91s3>}Uy&fJE0bi6#j(u5t3J}R$fO5dX8z*9Aay&)af~7YS zcRkK(_hslG@6GmHDF`WFFFEQz(oB3~|BLGQ728SxM{((0C%UhZ))XOKn)I`6SR#!5 zgnRDTA}G9R`%z;cwX6=ErPQp1xubou;Sl7>MbJP=QGaso%rQPHUL4Nib0meI&Y3}R z{~ct(r%L~5Mvj&rh)7g_Z*t+SuT(kuV#Wr5;D?WGJ=kWe)Qc&;~P3)3p z*22qQoiTd#b9l9GFH1APwId(Q88^K@8`=|TgTn*#8&_8s-%sl1p20!~_Jb;!%a>h`C8oe$Y}bDuz~cw$_W(Z%DoyOHa#X#Y7hg4B}}4yJ?`rAnbXJ**XiK4W&2~v znX#=0Tk{(-WDWOhhdE=j=(u^V?*$>3<0@^g!Fu5PrZ!Z$ZFpEgp2iqq-Kc^Bj=`&6)pf2FxB5vqdS`c)5Pll0Rm$tD2On)h#S;B54+d#b+?fh)J6aja|=h5&x~?fG4r}K z?OK!a#ek)i@Ye_6%Qz*eU?}o~h>{Y0h0G`Nq@!v3&v^@OBoyj`wpt4>u^Ii1El()X zTf{U1ucgp#Hpi21r5oNupF{eWDrsKNJKt@f&Yxp@7Dao*!*rpR~--n_ix!&zGeSSS! zW+jd@x>XhDgdUUZ3$1Ll{!EMcuq?0!oCf(EeW|J0NoV=Arf}dy_;}c5Mh7%U#RTA8Ou@Hb1Hp$i{t|d}F+3|J-+y2cQ2;i;a z$Ro=_lS?k4_y#92N7{amjGEE64g!fn;7fXuG`RlEF0z=e{`ymvk(qr31ac^&<6ot8QK~KTUDL`(Mxm{>ckYebO<`FJj~lI($Y8H_Uk3nNERL$P$;|~$ra{+5 zvd`;mT1>wGqOwQM(VGlSEr%}cg4d=!evUQ&@Zo1HoX6Q*2K3dSHwv1lf77=aO4>)HJu$FVh1lwgj%u@ z>oL*yHoSa4Fqfc_a7=vSqJ|MEciUcArMmcLab2rU8LNdd5V<62qox6^MN&kr&+O~l z*Q}SDe_Dt;n6U7F(97q$JjLu#X({eFh)Rfl?NGTqRT<}Hi{qX!?fz)9&lgCcsPrA> zSg8UySt&v}utU1pWV#$A+~Vf~jnt3H=#}|1!_w1OkMIi8Nkrv{0pHuC6|Zovf*{5> zZgV&5!oEzJFs!dJK;Ze(10TMhiQ)pVK@%e(>X=5kU`c*$1d?iRM%X5(cb9w8l|Q2S zy%($hN3UES@%r9wuAJMJW4(-gMhgknrFW`UXDFL%DP3olyIQW;V76b^@pTK8o_!pi zi3dscc+jkI02q7zkQ1rdL{`fSidda*F5s9h;9HTIQ13aIXyryyoaH2pi9XSzCd6p8 zI8N@Zco8rmHzhW!f7{wEolUC}&$|()i}>h*3Li>p-H9f&tk%oPC8zFOswQ+ZZkYI_ z*a65@P(7Irg@GMv_!-IRckr&Ye&3?84B4UaTgsZygQIN6xx+UDRtaZAg zTJBEm8=Ek2IsEACC&=B`cGl^o>D?U)9U_h*^OMmB34ssx zh)|z1MRj@`LsOSj(U@afibLJwehNZ}?riWTy2=AJ4d`Zj%n2p)Eeuf2RdziJ^kp1a zGGyfKW>{vZqo;)SSgFwSy- zfzXJ}G}!S}5LGON#^J-1V|sp0WM$3pA{Pc|p2Xw@IS=RX!jT>h$ZaCCkmC_*CWA8o zKGPchqEYU>%G}uD?bipWA-k&)vH|bY*1GIdv%S8Zm}fCy9%L*+k^!?L1#MwD#<6?P zdrWT-x_o}n#)bPH>K7VUdR4aSO~pzaHe=p4%8BMu(lM& z3>z+8NjgW*q?LhrBprINZnf6d5?&5piTJ3q7&*as;>}};4jv22<|r8xw($|CF4l4g zLh{L7gDvE16g{DilMb-r&AFXpgE5i1vj?u5Z*v+`d;5~AhyqPvokczx?P_<;eC`o{ z^V$zxt`RO?b8EbLZ)313Mt203AGSx4(cL~exC%NnurF=Ud3e`VV0e)_Sl zj=L5Tzkobpf7aHId-Mr=Q6*sZp37qFDtApkZ6G9UYD)exO2TM9H5QUAQ~YvqToxWZ z7l3V=ph*7mDHT{g!*!x8dd&lBk?Z|_vg$?^ZcUWNW)Is*frCVH&DS~Pz-u-bWj-5T z>SphtrO~(6jjaPsM-3vsr!1!n5!C`B*%&!Wfj5PhlsR`P(Lo2A^sPk-=^)9$J>7gD z_=0$8Tl*z@+@_m5U}yGC+zPIgm91chaA-2P(uo!kfzH>KE%Cx$t>;cPt!;+g15Qes z-ZKFb!{?F_BnO9TmbtkVuPe`6`p&k%fyI@#riW-YaT-BhYc{s*J8zTE->HsmRnj(f zN4JPbV?%aAi-U0mXLzJ)U7A0t46#OD0uIpNW--We*9UkG2gQ7?{z2S9pTK*&lCtp0 z)5hazmbVzqgFd%}JfBU}SH|WBJ-RDEs@Tt2xlFPu7H_(dBC~jvP77RjAo{8M>YA@2 zR^>P}C~`KEAb2oNQE)jTv=RzJ)g(m627X=UE4?_U7@rc5~Xe{or->^jDagOXUg;BP`A3g+s8YpUdjyh0T?-cEeqwK(|+jG_pU8B<-NSml8 z#-w43*U<_}^Lv(~nh}{_+%jzQv+6|U$y;39Vte=+rzGrOulH|XHf=_mV+Ve9h0XbH z*W4~Jux~j&c}pOP@=NCd4q)VIUb301zg`5Th-^k}v}Li{(C!-B6O45wmaQCsH)Y0y zm5V2WlbC1l8+Boxs4p~u;y zcN6DOvWEnXuxsFoWoxmYiT2sNPnZuk#g`Y*yZG3&Ph3;CGQ*)jVtIDbgdHV4vRGh| z9Fmcp;;8+lM;wJcdgVsQDm!E=7K^*<~e>m4{*9HPw^6H zGR*b`bl5Y5YuC%;8il-Thv}=#ntcRj-*&G^9=s`F-WdK`@%mNMUFAC4{QES-wR_!D zVY@nmOFPY?Ru+&(TjaO$sAp>BJPh}7v_)}u`WD8%o9_Cgx7G$TbsVLA63KjRg|fJ< z3C?bWBtxbBCU#l3v8fR--s)2cP!>caR5l`Tlmfze3yMcyfHE1hYD6U4GyV&f`jzHl}2MoffUt){aXx3Qq8K6nI)zFB@^yD;-OEaK23)kGN%>BY&$EJ zo#{ZL{9I)+q`qwJuxc`!>(!JE)knkGuG)3YOwV)E`X8HJPY6zN_ZE0$_WUOgP^_N| z!VG0eLJ;46pLQ&w6c0a8e`)&WcyVysu||KXWFsKb&*aR`uWh#FL(8biMGfu^+ZNITJHE z*QV3jeG!q;1L@4^_{a6?#E8!~9IDGm^+qT)RC#`2;U+W$PAh%vO4k&{8pEYuP7xib3?815sj zAJ^NfZjw&Kc0^x|@$jN>xGwM_;lPW^R)UM!zp_#771n0QY+X?&zBS=X?8TPJO*oQD z3Y>p*DedbOr{GRQuB=}b*^KjDf|BC2N@J|q|~*PMmS zJ-!c6szdR)3y zlr!Evbz?<9hfVHC_#4}X5?0pt?SB5WCVL;#JSb>$H1bAzfxH>%b-P_m;ZRxPx^8X% z?L~^svy(JFhj?2v+=EHtidz#d!kFC-Ba~ay*DwQ-7X-MWYRMa%G4}_1UWnRvF_XOa z+bp$&QO)QQ&5R+wj2y?5t4eQ_cGrY_XX0yYohdVZMuiU>=Su`qq+UFRbhggUaRp2z zZ0+X67WOMyveIen7CDdtDYONn_GCU&*2svU)MJ7`mZf9tGZcylxUHTMRn!(V(z&yW zYVw!7+1i{yfq4{c>sti9crtLNN4sgTAXd}h>~2MHx^zS+wpZyCI5~ARLd}3#BBOnI z>=I4iH|-%IwRLHrCCIdJ_cMpDbr0v<$Kh2NA2yIN-mgkl9Y<8Ln}aPAe!+lAwiR@h zAu=Pnv+KG@E;U}J@7gg*C-%Cj0^rhS} zA)P>+wTxFz7_Eu!i}5TW824(w5qN7RYhgKmXBR*P2M_ZFLbzko^M=dj*Op!!5G?h@ z(c8$F>qd>$3Q*&y;gvm^R-cN0NayXdeXGZt?F#d^*F3jO|K_<4qMM&sZcbpnu|Vu` z+N*xw019Kfr8+sT9kC9Z@&JP9FXn`bD?;+aGv&G?wei9uyPHn4K_8+xS`QA|77$ z8)W3?T|x@=G~RGuYg}<2>zL4j^_gRNkzuWt(q2DJj z3n2lPYyV*xI01h-IE2pR_G)7DleE$1R@cj*@sYTxbypP4WKGD)w2{U;XbMdu%T`ai zmV@#1^fzB%n=|v_Mwh3yLL&ww=~}B`rRc~Rbcuf?r;cbY*39GePm_m;NjE~H4|Y^1 z>Yu)MCV?i`EzoA5VDj1(5B0&*vRE0T7jVY)>ymGp#~8_0nHuPK>&?C*EA~^xYGQ`p zo^kMymG}g;dw!@W92Z+RSq-f;WpK8QS%o14X#f{FNR-xuE|Yi7uI>rO)@bY?W*luu z%qS2&ZXm4Y_77=WlhN|17i14)eirYC7|vrdXG*Q9sPxWP>G&M>Y{vG)RL=AC4W})B z#=67T$~>Z?$R&k^6wWNK>IBxxKFf|JgR~{E7za~ssp3*)%_Xn?x*~;EUlkj--t&{~ zZ2^)-mw+4E+S=1fj?Yo3}tT}$|71>Fd*#aM)v6ro0 zP(>mL96_~_U|P3BPafQyD~ml|h@dYiU-0ZcZbp-2)3Z=ZiFJPQGz`Kl%{_K^JW?+a zZQpGnKvW8O%Eq0QNo6o#s%)9uWQpp@33u&N^E5o#4?%RR2=$3`CJC! zp+A2%en20%#>l{waC^Gv^XL3<-gUCUPjBZRXD@XQn4oM5dxA{&q)0iClCNOli!Y=5 z4*K#R50Ks@$?7T`c4KjOlC9AqN~e~8V}Y>M#}UI zWN)NBFcn9O8E^<~RjzUPHAHv#HnV0(QfGg@^r>p%FKa`F>9KSIt$_PpJ2r(Q72n9X zln%WWU!B+$_P05Rds|?R9&L-nd-x1o7wZ9*Olk709}{^f!F%)D31&a)7tBQpctWVB z*O*&O1vSfBUJ4A)J1EZR=S-jHEvD?O!FnB92=Mr{x_0exhDqICi*sjn*F6i_{eTGj zMQrQM3c`qi!p6LJBp%}gx?tP}xu+>S#v?{5yA>OR%(ZRPW~sCHIHYfPq={n2vo{Nh zddbwJq-|{HMt}J0R8ujhe)9H!Old3D$Gc)ixSV&{Flkp9s3eKVJ<<5g9NW059JVOA zC)x9>IZNbHFd}{3)+tIBJGN^tEA-*$(JjciZ_J`-P#b+uoz~&65azAHmaIp|>3PlE z2ag5f6_fa`euxM1<_DJvJqR&KBWgPyoB*NCTw2d8Oo|YwAb`V}vmO&n(&l4YdVSDC zbjNyR6Dv@tL_x2C<)#2D0ykgbb(`NUf*qF2k6R}K2qMKDmfAKp;FYX9ZKF=@MmC~r_VMGN@Yw&3=`C1E{{`jZ| z%+l|RK^gyi@Tx-R_}B3SUG`$aesE>8jO=8SnGk9@U1`_pCB+_?<`krcQ)9Xp!M_^gLV*XFNNn=o8$Pf-Uqz7naiq*xZ+46K(nUs7wo*UQ`3dY#Kn*0V}1 z4BB;f9B`s?s;Cx|X)x_Rm_9M^1L5N9EY22lTT9HE9eE&m@h)?3ddirs=FuV#%UDfF zTBv+C^H!=DqOwOMtU%!ICpTTLi|m?-{AV+7DYBdx6Xw-DZRHjn*+3El5s?lYpqYOi zETh|G6m}>CRBl_tyd+10_hLryux%zoOx}v+N7|hTRZDl$w7m;{UoLisHyi`A7_6EaWd#$ zcG)@mN`Oa$OR3FP2|^&(g4`S?*`7;ZPC016v^U&G1|k|8YW41Oeq0j39MF?tcwyi0 zvCXzIE*oc7N9FQ8Z|af9!wVeDSCgL6S*C>|XdiK2BhV>UX-9u$Ex5(Z;{Is)_>)i} z+lFpWAVd2)*;+Q@BrWOeIkhgBtMn*$xNv7gLMO;Csorh&h2EL``pP?LT(uaE`nCpm zKxzo}Ss=J`+wzydcSie%;_f4HcZ?c~PiVxzstMaVD(pO)!!hhB%gvjnbhjhlJKa{K zLa}L?y#k8N;&}d>0Ui{i$yO1-%Y~y<5TyEATP>@F?grjcwSc*wH+=6b8RoRosvp= zF3^6G{Z(({;?uu(_)0jfb;Ib*=?h7FpIi=p%g%tUuzq8g5%lNnoylx^{-E!{Qxj1&_mK+;5!zr=Y6%Wa4rUH~m|mY~ zl9l-(O;&qn5S;XAi9i1G@l%4xyn7_nfU{K1?_Zod6^5?e>#KqanI|rYZtgb~MjzJp zT&o)3FgVA}9lA@KzdEpQN;foW$%O3$S8|aNHjX`W_;H*sZT`}BpP1M8{qDViC3Pk$ z(3Z`DZPG!d&cya*$#yQt#7()tG^2{Q#@(u@NGJnl;2NXtL2AWRa~}3E#zhPp0Bue1 zvXEJ|@jz{ymcw&olLL!d`FPNi@y@6jpA_vQ2i0=kExjC$y_>rZKbV7a;yP9bcHoIzC$(d^C>PxC+*tv9shzi^bT-{W zrjsR=%0&7`-y_STDRnTxivv&pR|_UNYkD(;iZO)k9FHba9u2XXc|b}^yT&i|DBk&Egl0Xn8;*QcH;=?Y(<>Y*kKoG5dam=EX z=E@rtpf0X9s|A1T5cZ{VMt({e6=_;j%qSTnIr)) z`hp<%)2)~`Uf81piv!w@dA8a>*=bBy`O{%?HuWA(W}6^6c&{lwB+&Z(>%3DoC)0ca zNg|n7!1v z9ZLeamgo^ggKNhm`iA_ht|4Q|B9vL^_!=3b7#@isy@?iB9a}G37}hOY0php9!22hG z&L+*@8BO@lS90v9hZK2cppe%jwVMsx%}=cJSg0)v-hYiAVT9cQ(S_Et-JX!^j=Fp5h!@?7UP3`A;^z_>jq8HFXJAR|5I*Pi=bz2 zXHk!ODE3gFg;r{FU+1iB#W`boJ}A*3d!l9e@u<@M`7~L&=0-z2zJv4hf!r8AxB>99b2zIosii{lnJf! zB54vo)%;4Zn27S#wmqp?cGHbOxm}am4m)<7Ay-;Kxmj5+xm`pV1i{BEnN;|5IBa+I zx+KlQW+v>mF{8%xR1JpjJoVAn@~;Mm3CFC42YwFtQin7NF_2 zoufhL4y19*AQsm0vIDoi+HP9`HJC0*83}Ai8{80_dV;{Kv=~mCvZ3DGx9SRp#wES# zpoBG*b;X^USJOzY?bz){)7!SqcRe{(vs>Ji@w`2hoUI`Id!v#tqvbT*{@@174Hipn z-I>$kfymp15nS0b!=^wOyR}hX>4Q_MeSJ(owh`^cTf30P^ye(Vv!Q&Wp0{4Va^HYc zzjWNGvkSfUy$A@%eAc-%J_2XovPVDi3mEu< z3_@8SLXv4~G}6B2%IIXZPZJHo2TGTy7o`g6FEB2dEHwU;*KmR19Hw7lhnAHj*-CMJ^bJkC3Dv5v$aQ#6JMz9mnE79?O=w zXX7GxH=zBcN32Mg&00=$C}T@fR-pBAbVG_Df4urtxNliijud)GXFY?JP!(rB4arCc}Uy+r{{(>vd;+oOOsRlD4^+=+x*oHR@7`#KX7r@e= zfTuHgotb4@)MgThKY%?EZnbgVgsC;@%XTbp2)q6t_TDoZ&hPsJWsF`%mqZsKB8cdn z=tN1>L@-+PnqYLIcS6+Y38F-aZnWqG5pDF|NAJdPANl^izyE*Tb>G}~_pW=_nm1-1 z&v~AG&faIA{n`7oL9KmQ{$3K!7v?$+(OEp4Oyy&vEWRV#6FNMjA)DG$tt{QJBka|O z<=e2zf?<9++Yb<8i%Nw-)}Lz<0?&W>xLC36?@c@a2db zHSM{cg(>OZ3EPB4(llnSELYo`#-#nhoBz#a8WuKvM*uh}c&b!9lM~%rfLs4($+K@Ox`-^ITzo+tg(`fd7J%qHIEIs+1n>-9D5 z5U&NcrS2MU2Hz_4J)mj+z-G{Xm})22seDOarns5-^h9qQ=~1DPo}w=M46mt_e+yD6 zCD8uvdtn>cEm@BDnRVA&*Ex$g-qVId(m10MZO!M2${4!fQ!u-_TYO`I)&(p2jb}7h zGz8D%knl$qNNm0MZaRROC`C(?=e)uRAl76P8zGJSM*P7{p53UrhA00GPukm-hvl<4 z`2eoj%Qb!63F(9+=s0wX?YJcQ_vntm*+auc!kPx(WD=OzFxEU5ssHf~71jxyh6>g? zJRWqImeuPJx7TZsP#k5_r9r<1PoLNS${PD_LR2$t5mpauM-d7OiS{>CUISCyp(V(0 zJptfIzCzE)-rO4FxtT1fU0`>_W8B1j@!T!f1vZrh-(E(bhmLTn`b{`dbJU16a=Fv$ z0-v}#<2<7h`au3IfoQ$q!SKRU{-jpPuc}MopSr?FJoM7fM5R!od*Vv(|6of}>q+uH z{)6q*Nx^s!{)Xq*15*iSAbp|?_UC@Hh*UMx@(e0X*KlSrtXFr!o6SHXKI3CkxEP&gz<^u*gREvPf})~`MR3R~fJVfXt+42c zJgo|}U5r+-N2KvRz2Q)X`-xZCqDSW-vj@V`hLP_n38Oc?OJ@SQVn$1Si%z+)d#AQn z6SJJ~c<4IIJNR1{?aS+S4_d!+j*^p(90*>9H;kOAO_@-mN2J;}!BH4SUB z7VL&iG5SaA^xh}=7VqrhFtJT|kb)0-+sDg#9UqfUWM0bH{dixaydB7#e-L30l_3yJ z0jeF{&?7cDLvQ7wkp52Lp!|(R4&|v(n!AfEHYzWTDe>}~wHc_lq{j1|WBv%O{Y_!^ zBonqs_Z#<~(5#icaWK^mpx0(_os4NDSTFbXL<yME^m>~wL2#td(NF;9v2?*m)o>+0AJWuF@$y~dzo@1B9A zD$3UW&+rym5{p5pRN;;CB~6#*M7!=m#V2WTF|zgV(L>64zZg7+j=cMD8lKIX99(@IKZvdEDsLd_eINXzc?Q%XZv+pwn{GJT;9`(t+p5uHV+_l@V z?Qr-B6~d6o!M$`=1pE_-uE76nOkYfSB)#q*hvuwc z354?CI4BXl@#T9TMZNc*fxQBq8A%awn#KlXNarUkTZNw#6CBl!RP_iwv1P{;_eqQF zy~PJxiRq{nC*F1E$1y>nY!`5#mggVR8BIzMRRBHDB(|U&w9=fAY}nRyz_JpD5RX~G zzL%#e;iOmh9$oyg6Rm(>WS!d&<~Qb{6J|7sMGJcofac@}r<^jZ-0+mgZH9>T&l=1A zoA&uPZf}*n}fat z9du;;=vbNfswnnq-rTwhKfa7_)Hb&NIPt~yT~x4dl;~>HZ}km3!+vZXyL)AtvILnq zZO`Ny#_ff5TXs2(r{87W$uxi|JqVILGb)&G{+5*$wUwn>*rie+j+`D~9rX8mZz_7! z(ZLcU@ekXA2ZeCO-n^M3EuqO41;TxhTw^BY&Z4>T%O3=mrA8CpCa0fW20XjU30%d+ z17;Oq5K^OaC8F4m#f3>bxfw6mTWHb?N{zWsj0%D`VIb7#ndynaVL|I4ib$ir38GP` zLFAS^`*?1fZs*mqryo)PKJku0L+ILTRSRu!+QxZxFgDq3YE~>=mBs&6K;!wZgsHB> zzwSS70L3Nzp23qHH+@JRF6v;~vWq(CWtgFq1EKMm2I5l3xas@FPq1K2`XvQF4(aEo z>Rx|nk9_DRE`#91#nug&+7Em&0`7l+IuHvw%r(LHB(#$B_J)ZTYRW8 zI^+=Q2YV)(*;|NJ*>&Q(cR~j%)1s~wYXZ+JhC>8m)?CR-Nv2u39`yFXTNVGWrPpu{m}iW06?b`x*}s0PlmoUu`XMaw*iUJzhVUx zQH&*rq7{=Qw-wi{%ot@(tBqt_A4gBlW=TRxcV3K$|72%q^ie3e{KBe;%IijVO_Wm9 z_;w~Bi+m<%59wP@IK!vAeJ2~~LlDTFK zbu#db+HMVfnOKhm)`7t%#K*EhL2FcIt6-F#?(?IfS|CMrnVX;MYS!&ROf>Lx`a9J^XC}YVZl9J(k_@VkioeP?z4beZ6$rJ_Hn{z?%MGwWf7lj+ z4Lk7JsO>u(Lhs{D^OQ5T_=GGq#?VCBlbgPT7N0h(MbiEN$uuO@-l;HKc-B+5%u+k+ zMd!RduaOLSCbT<)1DgZ4%sKNL=QBQ{ zB|#0b=SHI82=tLu$;C{q;|MOIZ;C<9XS>KAZ)(9{Gn zpghYC6L}aLu1O$**WWe|#NI}9J8FSzW=I4~(RtmDoA=$WbSYlf=-GcCO5-v0;zo!5 z?&7o#ifwJDXKyoj?0)}%LGH2l_|%sK;gi=t*lAtg_>hYa_6hzVyCejX`sa&jGLb1q zZJi7wu+ByOFHeD7j!bXw+BJ3=5wLS!(oA1y&wztA)}y#mn?0 zF=jFB^2qJGYoHX)IcJBFRMKGcgbB~N=jXLEQ@=ocWbtLepO*4d!-@kt*ZwG^?8BXy zRCPkqN#!l#r$C+q?*Py93LFZ=K9p10U307b5K1; zX3k%Mv)rLGK_Jm|sE7T;+#h6TGl7vE{LhHd!!JEjQL*9<$52%T{2ji5fS;c)_=9e@ zOAt`Ioowc5{vhN>(S*V$P$S*ZfXxGVclc?%bJU#;g1wVnU*~i^6h`@Y^xM4?z1T-6 zBeBy00)B;S9d$4Lsu9zQ#m-DuiyIcXFua6hk3SUy6_>o1#e+N}cM|_6jRC=B@zD5~ zWY4#w2xMdjWVoPHXaX}H&kEs;uuoSOTn>5gb-1k1t?)vl#X+{19o0uM zA7fY@&iqLBj><<*HJr(*2zzf#g4-Y$xp)F^b2+6ze%>|%md_&d{+s*ac}6>4?DFbp zJ&$UGcd8YAn<4s0PE@2Ln?_Ht;_vfP3)n~;bwRbmaZBgJRuK&fKmo;UUYU6Ai6hKbsVp%hTy;#x7aL9VrGId~P%Cd`8-_I@VT%*3u_16p+SmBu!Yv z`p$a=J|dX0PtrbeS1ym14e}5me`{4D?y}KOmwIF=A(J_V-XV`T&W5!8y2MC^s(Za8 z4)i;W*vncfAr|Fm+jRH@>3J;i5d4M><@xzK%&Jy*r*F@ZO>G~J;JP?*4(px|j_n&_ z{lIy|>VdM=g3_QqNN)oVUILDpb$Np{)OMH3Ef@|&Z`hCIA#-i)Zu0RBPE|5&Z-*x57LnEu4xyz1dPcDlGZ?(UIsetFfg!>LN-bCTsdtALq_tTl~ZpGucD zcQpUZ{;8IHq_~?LX~TJ&89MT#LLAX;!;82Zm8lH%nzI!+6%BN=_#_wr8P+cYJKLX_ z-luMi>18V)c@oY5kYrVo0C62p(!wCahun5qWT*0v1OrMVD(WJgo_cX6>sgE+XL3eDKsKXS94)B!~?lmLWjI5)Rzd7g`{Q_!bq(LXF zPQM%%sC2PwP`Q*t{a$iI_m0R4t^jc*w$xAPacKAYjd1#S>&ILS=+77Vc}cz!H$vq% z8)8BPYo@ed8P2m^&H6hk%^m|d9k%n_P=hTotW2bu*oAP8JtF(?{Wpk*CgYaC1B#o% z$Pz$YLl;>J)3+uM_RFP$=MFbT+(EOaFT~}Z$6?^mD?Z{aD8Ef)M z7~f*&1iQYuLV#iH5AjYx=C#MDn%XNvUkqh+sntNGr(yF|dy-p*>u+1m?qr(roW3e! zS3sNW@YEK}2WDELOioKZ^XDTC9Z_}+$gLfgpXb|X%Y+xXFnK?Jxv_OzJ7e{-BO@$5;GblgKDG0;H!^3)zn-3oed-7JLX#_l-Vx}tswd>#7*?qb5|NORx9jV?*dFw7%_e) zy98wLij*erby?U8Vby?A?OOvxpWm#5UGp;a1yq%pq58!GDY?d7`R@d8WSeqW8t+vu zIcQVcag@(q&^%qoRkWlDv_CRhr$P}0MElu1oA#*Q6J01~q4wID(noquS>LOzS_=o( zp!$E;;3zduw}bD{eXqL`h}fB;E@*f0b&?+Hk|52_b%17!Kx-c1vp zsB+%oYqr!5(uv}GaKuTaYeqVn7bqVwEfT{QyAAO(1;Fv>nmYS=lZ6QuS<08cJ5}IY zHZ?y9ps)2w2kI@Jxhh+w;6+b2t3jnax2swY2q-2S&@!i~TPjbDpA>MEABq0pvsn+! zOCm(LPu4KHozG7+bc6f}dWbxeh~0Q_DqNYoW|6_h&}@;` z`ubBju+=-FQGx(Cy2j&C(NW}6d*_h8{z5f;de4s%epKy4>v2VEWLG_Poto#jdjyRY zz9#0y=S2)u{P`|EcC~GX134YCotzU@6L*=e`hDpTN4~(6IB)5)NbSE-FY3Te)L8^&OVn8Zx%Z6W@)~)JuTa zV6?gLLT&lemu8kkd*FmDDd$M=QyG1H+JFOj*s+rgfegt*c|!LSCD4l;9@UJ=1>w`j z@m}GMk|}Y7Le{mtF;#254Yij|%ksjT)>ya75Jcu*E3_N>_EfyGmWC~tEo?jdCbjsA zV=hz|Cu*+4@-jZXWdk)`>NhXQY79SB^PzRV+LjC;7ISrUwr_^*3HpG}?5@zE-0QaKjN* zue8D3)Nu`7r0w!<_I}SH)El_)i4DJ(L;66WnV_G6PxOQGSUDoMKi0or`bq?b9`-9L zgiXd0X;jlsuiAUyc@KDHSfYk`8mkLh!sn04yzudM_`5lJ)Dw~~(20QB`gvp^{tYJx zFHQKCmD~EDV-LN`4g0%#Eh|rI{LUqS6h=1=zANDtT(1RUUr3Frq4S~i8!7L|-8Pak zjvfU(%e&m5vU)DJ>Cvk#^CuKiDz@h? zgLw$41)rHGGoR4y>&+(t&9JD!yC@{eU0bR$Uro{yNE!0?~E67*b5g}9j&i0t#OaIV6wYJ421F`Ip+EO>j)DQqT;C%nPVMe zK9G2FOW-Db4@`1%Lg=>0N?VT4$=tDXgWx9Sd$#RghhC_zuLcf_# z4D1`Tljp~P)loa$9Q7?WLC_aV(UE0*vg94Bw$)y|LH<|`?GGP>4@&ANl_y>_^~pOk zAfGB(jRuTAOs$$tZ~XlR!-V`Tem9G=Gu{XfBg0~BKsZ7Q-CJpsazE?g>SCv$#6#AQ z`0Nj7#~$Pf^PaUA^G{fxM9TlwIw6qa)nzaO1#MTF;XV~Fau=v#+HD@XT-CegKC@5C zA`uVI`+Da*$ z0`|eSNh|H~L`w!U%L#)<4M2lT8ry2p;>rD~)3!OT?aQ=Ag?BMsveuBtKTb>?o^Ty5NU@SWN(uSAe61?Jq}52|nGWDjt&Z)HshYosNEOIOArDcbg-_ z3r)P%w(Bt8rI1@|&)2?81zOR16|?(nbZwfZIc5;kU7PF`nIxLLZfk z#XMemcQ1S2Ro(VH`t>Y60^l=oC)u9c!il4Q#2rAqn~mF8mnYi~SgH?rbeH{>cU5{d z`+muCSoG`~-ni$$$)`L4iHc$uO>!I|T`phDh-%|L!4B+7kjA1AUZEM{ zciNjgf#d%MYPt^+r;o%DTLpnH=el_h3KFd}itl9+?|#7*Th&S#1JcA+5CX}K-92HbDR``Nv7KQ2C*uZ$RA?KwZ}+yd03lErDCC(}X4k@6y>{^FX8{dwD5 ziWbz;m2F*nZ#t9-Cq2eMv`EWU+rZspHjDxJdC*P}00ZL$0|Lb%H5~abTLL#CqPuPP zo~&pwL$w$_<(~~)Nb!SqIRSkUK05s8&-tG`u+rVoPO7IMt*9dSt^l1Jj0vw+( z@R!!Ln#$;kQoE+^RFK#b{_83vDFR3;%~hja4O-k=i68bx0QGdah9E0|Y}WsN0G5!? z)2z-%Zz$yk)t=3uL?sY01#fwO00HD*&eo;6!-2D}6_d~`0pl926u0y~6qtZof--dY z92tU0$#(hH;X|K=NLQ$mC%Vp_$gvi`OFaRKQ_NKt{M9mhYB0yuK?DK3WqVUwFn(*i|RbU9jx8sL`Au$pi7=}Iy;khpvg{26|*+#Ly#JeU5JCZKvt0Xru?4X~GX z0(bJNIA4miw3UmgmbzF;So)-AL?+>NN~y@;Vb0B^1zrD;8Vzicw;ySK5H6jQf8Aa) z3jU1r{SV6_YE|xf^D32;>D1QRd0$QvsVk?0RxAiO>6LA1bw@iy>hbt1OZxSO$V4&- z1cm$&oAT5mVyr|*_m#Z(@o!mEfJ>3*<7S1u2>N)WQ%BG6LCuVd>Rt{@tXx0_u0_qi zyw7BKzf$=W-|tLS=Xd1ANep1YdDGpyCZUY!Mi{t^aSxCiZetHBzAgX&y z9=ilEfgX!DJw>0C`Ekt}1eY~ms+82uc`ilrH19VY_}!fOnMQKekkX63CuQ_Gj)$F2 zT6X$fA7yRH($D3#ZX2A+1sI-^0V!EPoc1=G2o#X+*!+s_-7j7NfgOMdbt%^G%8a1; z2f-(dpU+4^EWr=pHD+B=2hZ3>!H~1o0IXYnSUvR77v%(ac|_$GU|fA725|)6*)6*% zEKGCcmbjF=Iox* zD}eZwT9M8t z5%urZBjy42;mm>$f5}iIq!;vBE_mt0U7+}NH zg&+S>&2)=DKznv&`CclQRQ*xOOs0h}gvr-Fz;o$%mwp^w`s-&fVdsRgA6JrrL%ovo zoE0Iu{o*%}3zD|gaDiGaV#tMl+=HvedGsJkOnnOY|9O;@ksdv4AwY-O zk#+?b@xja1X1LWdK0TpG-^UOV`bUG3C_4>s%juNux767@MabSKMg|w)PUWLp7V@`C zj5N6wpit%%L_o31&S%}{ckpq9G2@uln6{3=mTiXavk!ca(&xhf($;X>@AX}gIh`lO z6J9fx>Jlf~8rh#c)>7x1ut5Z_?G;!&U%6|TKRd9V9$i*vX8f3=chXy!aFJ* z!o~nPXzkevL*&wxD+$$1Cm$+NIFkKo(1EvBiI7J31kHo5`5WD=1^W*W zLZ(rwd01%>elh#!J9o5M@y<$XrutA)+##g%O2Qm3XfijXuFuVY)=H)a=08~{0c-TR z>ocg4ngY9Kq{@U1aD3?STT?L#K;+`TN$>VQ{tOs6lY@t@SH^-QnvnRGjBW>(bc&Nh8WPXYvz zf3*P#s=XIdaecWBl;L}6v`$AgJjXv z`w|)`)zy~7l)HLI*{Fw*b~W(xYihr4gZpy$U*aE@Q$@Tmo!cJ`vp0@=qIszt6-O%I zfX|MZ`R9Zco9CKjF(xuyv|?X70`?Gn(rFeep&Om?bL1X1dmSqQ^0f*jg8N6vEsy^p z3q}Rl@gdxsv|}H7zs>A%CwB*>cp;G73BL9PX2XLJK{_7bsg3E@!yFoxpdtCHl_FB8 z{H0Ty$9;*+eWvRrnwI@EY5h5Q8p9R*O<-@GKe90f_<-jsymsrmy{03AG(lmE5;CKz zxD;SM@g~>+#5od`d@SBhi=P9rq_O+yLy7H%o2K%7sR*`Q?S#Pn*U>pIZQq<2*GFYIpeh;aR9xLFM%7l6Km-XoIf8Sq!oteAQ#52 zSaOH^5jTE?g$+>Dn(?KDTkxM26iT2X#K+7RbXqqg1%yE{Y1)_pUSV)4W(eGmltgK( z38zK&$+BcH*u-rkC)D#MG%Q58U1AF$$S?7%aV9$Be&gu*r90U7|leDrbh1={-)k7i#bIu0-%~UFv3X`sl8* zk3L>{ypxS`z~j4xAlmKE<@u_H?7_ICVfui{*#mCXMu__ikL0K5LaE-~3%t!XQ69W^ zcv_E}WJ=H+}>F^D$zo}I-TN$RhrBrCa*9_vhoF-ehFir*|P) z0I$0G04eSPK{{d0G9_ZbJhuz}O{3`&x`?l`8yk*)KkNBeBu!eURyzC%)K8(~2lJw9 z&n@*knvvxX8w1&Ph;m>M_7m) z2RBK%ao71`HK_yXAxqTtHxO;{#*13-K>y{3d^V$nHZ!@Ri_AaC=3^2_AYyg?3%xXR zWa4*HP0RC6g?90KTyA&4Y;tLD-tVDxf-J0>qI9GXVlvDZwx0Kk2=HL}W5HphIe(6< z2?+h20x&|XLSc$&BJcA3H-e?L%Q}T|H12mW^?}3n5dpR($E_+A_feembVW|uuNFif zo52@zf+on6iOB*;M<{lK>$2(|OD96e3)^dS`}k$sq<5c!8lhQ$BI3}Vxk&Urxi=G< zEvEgVNK*6L>V7%6fPtJkodv8!?YD@+4sD<#1;oR+=BxM0i zUo2i`H8?+oI`M`-k$LjM{HIonH;^;3#Z37X(RDz`1OM$69j_1pqyu+oeB<4y5f*_H zbaZM(vnTrmO@N<&36&C1D|U@Np$H7hoHRyCJN@ee zUSbZ;3vonIvbN}uc0g&!yF&Z>lroYX3(=1lL`nUkf=Ka3b~)}y9phR96zisAI(nr9 z3I<_?aLWcfg&9WPtObb)Y@yG3q}QXy`O! zJ@grY1DZ&woIGfe1R?5Mz?ZF*r-Cf&@{*Y-rFV7-%pL{0_fR&Rg71{ zBkvpGF3DiB=SN)x8rCTLQ@+y zLsrcv_9vMxylgPxpDBd~oZo29h0)QXB>yx+6UBtr^Lt3BeQ_dAG*Y8W2K}s(O?*`D z?DvAEDW^mk+TI^(`vjh}^&Qf{1-zzcoKsmxwc+e*;#_6!H&q@E!_?H*_d^U$!c+pd zB)S{aUVv+Ca2cR048tZrC?ZK#eH6K;w6SYA)EQitjPBEL{!9(@idQR1vy)bW%*EkG z3-!s9Aw0!M%kjwKGk5&1mKo=;(pK(u;dJL1l#ely!_f$O`Q}u;{$dio^wuQj(Mr1U z2T*!U@}EvCF=8@q!3ViOi=(kS9!p(tW_{z`-mA5AN6!NlM3~VoE{9p%=pCWQ;8tu> z77fp@u0w(i`vpK~`BFQC{jnWMECt2~gL%kPJehwaOoi~Ap;E`YJ009CP1=9%>>2=3 zXR0q$q(bOhM<>Xr^|04YCKQ*BHL0z#P*xLhySPAHbfDdvj#1X#EVq_MQx=1e=xgZf zF8S@Sl{5xC3K)FVhjl~to->4g{rifOBjp!K>Z1hibuZ4!WYal4-kcrKZ3bDna;7T+m zpzHLDS<)!Pa3Kdg*qjY!C!NSRA8x_RyRteNO%jDHHdY zUa&PGQush?3tQJXC|5Fwi4w<%T)^Z2_w=2PP@Nujjr9bzfaD8Lw=bF@dbAo#o+;4{ zO7c_)=S$jmTNQV1BLpz8b~jcKHT7UBxm##lx3hpFzZJ>akQ{AXXk`DrV(1uD~3pZr-Ub+ z7z&$?I9drhKmd}T6jKL|_(|xGbw&ugjvKW#NCO$TN;1jG?XEC~%2EjzwRSZD1R(Wm z*cDSQ6GS7LeJHZsWLUyCo((YbzKr$ zxJO`*GcLuwCKId@#kgf|r z{ag|^MNKjwS{lLWCqxY`ua9A; zyBE1H%<|?Lfh6*KK3W(URIb<*M@h#p*UJ)Yhy7x6o6v45r9rZ!9|eucfcqCfD>=a`nZZ@D2dZvnoS zIIQ?!o<|L=BMl6O(0)moFU7{Lqh{UAOl?|~o$0UbWJ7DmT>>~5;G_;5Ldtj)QGx-) z%u%6iKDLtk^03f`Y$~Kl;3busz%MX5P?qpud}~INCd;%~!Q1yu4X!1toXDIK(_$t` z`bu}mem0h*35Yh0fbrSO?cfbP_c|G?gqdaWC>rdV{*u@@cI#iXX)n*rIK~i0(Nh(TDFg&^p!Evh?gE;_lhT$TNu9c|~c5W0YrM^2s;GtGMZozSx?S@9taf zvNRb*m9oXE6@H@BgEgqAr8+Eb_FcLX%H;zcWqcF zi_fv%P4+!G!MV956IC_R`W}cznddy=J;%iE@AUiL=`|UFgRKURK0h$34eg%#3qBWn z50x~D&+%^kxcZw~@?&UimrWhS?R&uBu+*m*Tt0ElpSH2M{D|O|#Q~hPge!l>-R;A7 zr$x~zd5CSgNb@o9Fo|PJgOBkYo{Mju4kI+HcKAK%{A+5u(G#3oy)I+ z6%fR2NHYL_M5|e=RQ~O3^N zC;)5{n)Sl1H)86ormry6Y2IIrv>kCsm1%K;CF$v*1MTM#%9BFK(#>dw?)Y+Vf1tTL z=jAOyF@?dfR6dm4y(4sq7f*x+aeb(5hLXho5FO0FtY{~kt26V3GGx;+h_1vu_Pi^u z)D{`Lulv|nE~@de6~mx>`QCA?HP3#wt%53aJY>zU5xR`wNU_44YL=DIF1-_&eMPm5` zP)fUM+wu@tkH8TjYC;ia5mI3O24k@lhY&{N#M>_a%BK-y9A=BbBSdZ_!qTkH#!hzNN`^_*a(qXimSHWSCS zIAMJ4O9Us|FM1whyUoBVo+OE@|BaLXZU9>F)$SfdPc?>6A*?&#$_QBXgXO>lXNAX~ zcB+$R((hWF40#p@hNhTyI59i)oKkT;7L{3$lw4U#t3jAXrpaT-tTL%r>_CBi0y!I9 z8XU*;`eef9W9;T42BB0%<*Qj-gRTS<4c(3l<`^|@JQ>=BnBcGbP{5nWQ5TT;d_NHk zAqyKzAPkOUSNAi#;QOaJrt~&IPd_W#E#OdgPZ!#ao#ZZJmmGA58>t!*JnJIssIAR@Bp9`aW?(^Tn9e$A_(v6FsjeMQ8StGz(*O~x@| zBB7Y<)Z}v|4c_T2Qrn1^a{wxM#5Q=#;`rasYXP8*n63mIN%t2``46V@fA7j0fQkkq zIal;%zdk%d#f4-1*#@|S#_M{HZgygUjlH7gTA!=c})DgZ01e5uI7HIveD<|dr z5|nDEzH5A0yO*iDqD5|JEu!&Zsh5^^ATl>J!e;@Cv4`pj*zNmFd;~8ZQoH;Q2lzj)Voj>)8JuZea#%m)Gq~}sea~~E z(I(_i!bgb63hDN|LJXa?L^z<5NCn)6D zbdoe_l^6Z2n(%e~j~^rAH&;h%0D6~oSM+_M$64e2Y}F=!Ju&97lR4HchNx}7WHtoh zA>Uv85&QQY@X1poeHv0FylTesz+bcWueObEY!V}K4gnx=X6kC=@huoQ0P)?&Is#C= z=@O%+DLeQ<*P1^T$rGSgAAIuNkc92lqhv3j1La!No=@4uPnu>ZfQG#Nnq$|vQ!{>l zSg%Ci0oa|YV<}-k-=w?XT0N^;U;%9DUO{YZ>D*$xaK|VE@>qjeKlX#*L(X4uWxe6_w zN1sj)0o@V_Gi!($C1<86CtLjdw$TE%<0-5uWnpkz8e1zXUP?CLp~ z6mf9(57xhPaF?tO2%Qc9n2jdA8q1+p)@9Td0Kv##mQtw6G~c#<>-`qd4*G1q zW$@ltCj^r+;$+F0J#li7hV%-J8vtuU2^sZjL)BG_;R0si=~( z^)ANBe08XP8-cvGM!zMI>b`1pv!7Myylup&P`@C`E6?$`Z&ngO$xAN*#HnbVZ6L#K zs#nGG#g|*eOC?Nv7R>D5O7bjR^slW0WWAE)&obmb>zZ7@Q!`!iutLj;Gq=fVn8lWF ziO2VV@aQ1cZze6&rR3AxBDW1Hx#g)UN^PXVtXJ47!RaA zANjv}$&0rUCXNbtA{sg66H?V^YfxolmOGf?mdV8Oq9}B@E>e9)(gr-9G zj0Awe=rG$vI=D|md^TjqyOb?Om4N5)MHVPNQj=TKC9}3vuaQVfZXFkyzNaV;i5kX# z=r*kU08qvs)h5G|-{ja+5kh$6bDkKnKQ^N5Dqgapf#~zoh&iqQvv!asv;y5?U-;lf zbyUY7N8-Is+ut4+?r)d1%vn)nqg#`1?|E5H_0{=dhOGR_`!892z{X*`O=nB*QIMM; z_r9h|MKhrE;g6T%pDiv9S6$ONn!q)J8n>IhP0@Suui-@Z_f8jjL(6l>y8a>nj6WUT)~(2A*W&hJx(HxF~*SX4XsP z=95y#)pq9xm7oAa7E3;z|F|<`cn_z%XI&>=LCg-kO3s1Kpl0@LqgX~W@+@A5E4^c% zEm1xhH3xuM81rPw4JtBZ)jZiVx)|MUrZTK~mzP22Vkt3MYE=)e3TpacwVeA>$}YgJ zrGTApi)>Fel%(`q*{SEl7*yA@VV|#^4+-|XQ`E_>=1Ja3Lu&(*%gO%L8BvfU{2=62 zja_PWuLcAbFFl?*1Z0xnSAEm)_|s==RPXZj!5b>f!S?%|u2?yFIIcE*KzhkQ;fuVY zO%|hm>G*eJI_eFXx*Rxti@U>lxW)PMz{bS|8Y=%++<-gR3CsX@zHutmze+>uFCB@; zRBiyyH2IH@b$fM$GT`w&Z+Xi3uM5b85kL)?l3G0R_GkaGz<^<+Ouzs&rubUu{;v_Z zSo~WZ$Y3GC|LiljF62Wtz&!Ax`%3<`Lcj>G=(iR8sYzQf?Ib*ymRDxyZOj$5HXuFjCE-9!0kV?a40K1 zpMJTwFnm#guliaQn6=l}^|bT>8Dhy$(!b(f`Rv~;)=eY_U7t})O$%)uq;$W=m4&X) zE9&F@mHh>Cc29M)m=4L^z@EWa%h$H}pvLN}cS52dF?a4ZroWTX!;6XXq6o+^W3O)I z2fGnwuX`wg-7=d3?fzZ${B4*no_`|QrTgad$* z-9CG!r)+1DNa)J2sNH|oc~y*b3UHG~SKq#N;J?LT%!-0c$*16}_SjRdCU z@s#RgFCX`YU_ll!ua;^TIj|e&=DL#|*o`FPgYw_Ye!m1o7{%VT`qh@#G+=c+YfZSL zuwx)9f};@(oUN!zcBVN%jXpl1tN&bm4Vw?NO^!qMU5}Mf_L)|1h<>Ti(;2Zqxv~d7*fmx9gQyV-iXJ zn#Sw}@cHiJy1@I-Ap!K5ujA95;vZJJr_%nN;x97(dxQLiHSSq>$kS8frSW(1N^~6b ze~qPd8~*<6D<@pV6b6tOsES2sk>!Q?-czB!3tuS^V?M)L(|%8q46(D*(owq3@_(@R z=J8Ozf8X#oO(EGyg=i326OpWyLDr;5G6o^pvW=Zai=8Oh$zIvUzKng1?CaQNU&hW@ z#_$}f-|y(}rz!0Rh z4}+D-aCW;OpXMI0ew7D>wgpxTU%Ybn4 zG+K9g)~A~4M8LOSQvC-dNJ3`$$vXzncSDm4&?o$dwpw~QgpZl_7WfiRlH1Mt0~+jQ zgeK!}RhNUr+?covp3n`Rb@9(r>e=NWlBdhP#b5}g-ds%(_eh?(G4xv>2SF6@qmpEC zF69570qqrSk*OJh62?x(3e>*cC$0X(Dl;JO6in!dX*-V^4ZQC+rih66KB)Se8ai-I z{j7=Mtvd*&#_)A+@9N-=_m_Sx{Ok^NeP+`o{{ja(hL*RBK!&4}i23^44vfGxzeIjj zMKWA%$@N3|zsd_AL8T1iE@LBR3_*(A;c{jBD^gVo1V}0T@$CW`O8>l0V*(k8q|9B{ zUpzo%nb6gGe;{~^7N!|q3o z^CZHquY}ET@UVL#YQOG!B}W46f?SUOF%tkK3DHFd8mwk>7u9ctQwIu%XS^@#1;DQs z_$JfKf6yWcXt&~&_Ov!|DsJNSBY&gC1UtA1r#^gmd}n4iiP2=eM|p_*y`H=kL-@| z=r2s-R03AO-s)hqC8@vaS-w zQbrQY_c2OgaR`#^@xwov)Xn1?sjLBL_Cshb`~M_A?56>5&pMy70^j%kVqB*lK!$v5 z)a>citD>oah0>gej;y>a~h77G>HnJwe(V)Nl#tNF3z%k^r_#*O8 zOOYRVeiaty7oL4D+{hwzQRz30C&(ZGtf`d0Np1TgBANy$&HVr03*+1gh#19hC}F1w z0;(xZ}6`7bt^|2cYzYV<0Z@{7Pz5R;pf6CWm^*H{C9_yFO zzZH?)n>9!;RFAesFwg03D;QxVIrr#KQcZR2>t6rzb%sD;^J)I8UP1mxQ5C(lvs^jy>$X8pGM*K>jb?*508{F^f1nyeLkcm6zn46;jQVp2j5E~`C; zl-xxB$Me@%FCG&OLOI+hjs4Ul&EG`3`{e!6g_pJn!+*c%grw-Pz@Xmx&sN_>GBf%7 zhyMIc^nX(9U8)7XWUE+$pF29o?-R(;EKs z|Em;#%D%3^L>evuaPK#a|AV>)l*#aUc(UMeCSiS3MGp~j3DJ51bMn8xLjf^m_6Hu7 z!OE*Bt0`+*tQD?2MU~q-j^W<^?#(K6DUwub6%K-nL`KLL=TeT?*Spt>#= zK=S*=XyO1`-ohjmaeC`N?6?Jv&&*0v3xuCS>3hSf8GZ+DHvqo(b{1JRk>QFQ^>5Z~ z^K9L1Ja4}?zmL2P!LLSW54jXln~@j(0f&a;RPU4Qhe%2iNpgRyP9Mkyr3&x6vbRVM z)Usuhaqn<-SLE+XP87gZvtX&6lTft%YSQ%ChYZ8ImWj)tlT|QuZW^8>C6NKXIQ#1b zI>!JTK|(8F$UH&Zn$32jNdthrud7%0VQdj|3ii-`}))?a^q2LQiM?IZsdkm;5Tx`hR`H(c2%tEYJV z^*8xEfCu+Wlr=A)^v{U<&|~FuRLXfGZ~Y?t4M^aqf-=`AL3HJ0J4IfWjGcx@xZl3! z@k_zTfM&5?)cwiKj;2_hd`SYRZf1h?{0ak5oySy%GFF}-!K?{d%iNaqSKBI+;Ct~0 z&PTxIOmcCcV-~eou<}O0zZ`QsP&KLiq+(DY9j!YtSM0zTB2Vr6uktRn`AQ^*|IQ(YGFX`U@on^#c_!m9`ADeOj%{b@%Y)57yROl%MQqnF2PAAowb zI<27oUn~ElNM?<{{`OH_}P8qua&>^8++3~HL7Ee{T>Q{*8kf>Wp)1QmqS9Abdk%BP5! zR!3N)sPuFlbuiX*7py(6`<(@6jcT*eJ1B}cv~eTDU5j8@1mrEtA(ju>&|)`+Vwm^z z2B*n=9B#6shkt%U1cp|_iT5PEm$}in=Cw*EJ4Sb2NOTT0Id1HGHmAqGW&va+3e{v- zBabVapCtedtn)M<@x^ZhuDR%wkJlvI)zNxMZyi$5;${v(OrFDVAJXxzj*CJ`J@U0SW< zKHPT}zY4wgG+_QD3C(v}RG&6P@^!*LLys(V1LtriI{&)}^6tmae) znW*48cHbv7_n>}xmpIXcgRKIwJqN@U09mM62;A-H1PcEd?bF1^)HgfqPECEld$2#^ zuC)mpG`(@=V$G-O*~m&Uy#OnV?DiT3L~$138Vd={8asNQNJ>*RDUz-f{ml zZehT8mKUg-$hf#as|Azc!h}fDs8Qau=m+w*B!d>_Cw}Q08BVP9l@g-LZr8=iNWip6 z@&q}~Q&zHKG#y1eM)GeE_#+6jVN@A<`gEGz$$_0uRSYXVI#?D;U3YvM*z#|%V8@M)Ag7;)wx zkxb;HJ1)JKvak6=vE2in4gDGAt7`wqB=&1mRpmAdGqD|*D)4v{!=T4@C?_X@CBvPS z6M+CEx`5Id+c|tVIk2=+{z2DlE-uq0Ghb^)D76qWRq^) z+go}xX6w6Ls554?e*2Zv2mdk|puILHlcPCQ(~mA}<|_Qbm^G?J>!*|JNZ zhM0h1n=2)uz4R2+9mP!T&ol@G5pjWNY4_Lk|#uAsFvaee8FCteI%Qo#_# z&Cpy9ETesZEgiBWTWR}U(?e*#IfEY{_^_`$f0tZwr(t41I&pe#Gy5`g)CqZX&*QKy zpf?4+)hcccAqYN!l#aJjk4j;Jfzmi;|7QkKYMhuOSzkmG^6@#=-ZMCU1y!3s{x^lg z`~IDk4L7xvyJHLqJE{;2(c}ABV7yNNdirgqY&kZqJY%T9^U_%#l@hEs+b7+eA2+}kTZgklX)jzQj2|vso!-h7V!x~uaja%Pa!*ucPRNY2jH5zA1!jo@SFYu zd2T?wdqTeRo-U0~<&m#LWJoYC2VhqqsBZc%DZcU@H7oW%_Z&4f{g%YD%dcox>CQkR zY-1^dCA$4hF#*;^-LqXsY#_5KnFpA#8_E_L98A@-up9KlbNSF>JbLD*2P~5d%^dFk zWkqzK*~xQ=e{i$JBP-}bP}6~hW4Y8OeMsiA+s^x-)@lRV8tZ_7lM29(pvW>^ZOVx< zE56f`0jpv;aY<_SAEoH;x0fdkwnM9$z0A)+vF6=sXd4xT*H&yGzVeE_LAuh00+WU{ zZ)SL2Xk$D)2pp1|Z+r-k78Va*Pj0#w_LI%b9fl?igN99TM`i=DY18|1pewpT6rY8E zj$Rj+n&ru_5iZoUv~qBlo@oORh&VKN5g^Bf`vWDT)|74x7G-YRVj56BdMqu(YPfvv z(^4?2xhrdNNchq6qcQkFq^{)Zxy}B`rlEH>LrZcbZk;*pFZRls{yB~u?$JIjoFlx5 z|9ELu;shPmL+DmjBZFT-q0ZAs#Nunjue9412~zRrLMTI|hm@X42Gnme>pW$2k%p{~ zS9=i!zNW^+cmmAHVhBZEeQiU`&iP6=U@?2@M~=E(B^@?M{>WdtGIIK*)o29cv3n?_ z76t{*FI=C=io)%5C?7gTs!utOhI=n;AD+GhZ99kU+ov4TL$Qs8w8~OdZ5Ir7Iody@ z#p;may*{pkoXzf39u#}zgwz|^#3x7$W{PLaJUPH8HMK_bSqs|;x%S?c&T`Xs^Bp=I zU@lHwaKy<};wv1wN3vCNoCtaKEFWJ>&kx9>ho?7QMS*ni9}=OJy~^()p=$x&l`^Z} zS_ZuJ!@T%!zaZ+lG+RB(d5bk!l?+(1ZZR9R-L8@W+LUd;_8?Z4M6%lFI`yM(0PLvL z+TlA@)6N|a&!!rdJ50`$y%o=Gzo-1AA|bAY)Ielsb_6@f3mm?b`(iFgo0P&{aiKZR z8K9PxX#KE`vu&K2;n#nT?M9SdCO`W{Y8RMDmZ1iGLHZFY?iHn{{&6}I^K5_)E5w3C zE;&`*P&!=p7$K3lUpYEeh`z`PxDQg2Nn7-xJGWyk)YRL=I+ZwNRj%?dFO?X`}3<${d%gVH{Mk* zozeT~!pT)IF73b}vMZjTVGHXy&KXgbh&+4(0ed#Z}`s^lLZAcl-C5qx!QBpD74_srDS?FNR|Gu1Yrs ziJlaSdt%lZz}=d=kv*Kd6;*B!_Z=?1W4am57_=(*EuEP{`1!;7+o1=@sYHg*%T-+7 zqrn#EV?au8L*n19Emml5pU6r|jvu1Ku8`r&Gg_lvbmSL>aJ%>V+1!U^m;TAZdgNr^ z)VOvDj^Dbqa$Y4dM;w`{*^3ptmGNYoQP|V1YZ6;tJ5)7Rk~>C=bJ07`920&Smlx4V|it|pfr zASL;7c#qj!V~A&|5FPfe=gwPn&#!I=YJ~1_?ywujaK8n>P5(|LlVAOfmKy8)>$}Jh zG9*L{u3G!qw?!zEs0|^|IZ+|^D>w5*&L&9p3s^!3dv}+O_mhs^Xx%Fe9{23bGcQ<77KAKVNHkNr!MT0z7S6eWz|~uK!=c{Q>Q=O! zply}_DyBQi4XD`RsRm9+Qr@foy}-9szWaBv^g2(6zgx&7tJmv>R{7A3HNhp9 zymzTlaeInVL$3g3cgKIUU1|EcxpZn?gucBc$iw#Js7D%(iRX#cjHy$!axgMoSQp@ZTUX`r*QRW~VB|OgqUzOm0*-u zvg6%gDTiBcX-y9}m#9mL29AfimBaC<#;=tzAu*!>%?kU^}WT<0ZE+p|3bcpBtz8IKW;kR~kgh43|3& zB;TX%>ptASR8a+*!$-f+Z#`gpk*u)XrscoG+<1H`aPeS8EA^?@^{U-)sxACJ=c2m+7#h zlefy*%1ajN*0$b=*#Fd-J1A_n6QtVYe^j80AA490`Qevn_Q3c=%5^g>qlv(YGf4^FP&e`_u-u_gv z=xZ~Lob|+{vKEeD==9`JPw=w1{#spGN3|BlQ%vW86D?l)Q^nj%d*QerE@uj1NDbPT zPMvpLowDG-(nZ>g7M9G6|1hi~9o>9?GCpQ4{@v#JmkSN175XnUs5A>C+7Ae-ns7|O zf_^~TH+oaitseB=HZ86%!r-TcSBQQ=b6!8Puh_0p>+ECK!Kgta4fD^nXK-^eTdYgm zu`Lok$Y#7_hCrJPaI5~Cw%LJ`Q&m zAHygJ-vut;zxUc#n^Sqz2pwkn*?4~_!#Pz@)R;b1jfPiqpK@4Xawu*r?Cq)&EqNY^ z-F-y~EQqknGOK=kGF^)uBE$QC&2No4C3%|@M0{ttvJL#A#^X}j+rh^-M8@%jS)QjW&XN0Y#&jC0n54g&;+13R{iQM4cu#z^{tR|baDduhB- z%d_XLJr@V!T%kV&Pps9hoAs+cC>;<|m%NIv@5|YDI=7v1NK~f7t02;s zC50r7Pg>ZLz70a?kw_k_|1k9IdOlPoCP5Wte7WKOZhkkRh^9=_*}s_(ArG!BqZa#uyoBFU>2TKF9t(%_xMfxJ` z$^S58wQg|UIf~&g&kC+;NUL!USK2IzM%p@m$C< zVzpByK%@V7*bK&TY!*dd)f8L$C@fPiv|3@*g5k9}P&CmCs$qQSk*1ohf^k_AB*!(z za03U;HO)sNIeJ`+6D#$8OpBxT^m3!y5hd3j`DdDJ^oulFCEuC zbgyz=u>DkhY27CWQWWCXY7&W{(L2 zIt2%LDlcX&Vd%M5s~0x;d!nvhC8TqoyvU3$-hYZB-nO#KbeAJ4$L?@tGW%J;zA@&i zTe!Df4JN`~2!&=%(J=d2?bQ*NZa+9+02z2wIb9bh(0*q;!RO7oK8UTP#Yxy}Q8=V9 zA~xteKUGuFuy3d^h9+BeEe?bv$Q8rN7|E$8Kh`{- z@$vIB2w9g-aV@ZAh*>_HYZCsT;Nl5OF_x-umx|k$93GGiz{%;bWee-{L+az43ki-~ z=%d}fzbee}pM19%AL5x}+t>3u^p*~Xom!stn7#0z!e`_Qt$rIX@z@ztUOmir_o`2l zTtrAqtCQ%_mN(+9q~_?7wInVw@^NE&B8$%n6zywyINkEa5Gl0OGpFrH*EAjzxX|KA zMZmZlIZx7kv72gRUT~zCLF!(%CYG#73&HKeQe^kaN8d9kUB~J~#I$o4{-xq5fweNu zS*de~JAKzrFSP{=3-ushEx36DbK*gnde~E1ET|Y5vVwYbyc`VSM1HkuR%Ug_>OFK4 z*P`Og7q)#BdX72#^m4^{NO6IS8Z6u(F3SLNv3FVYN+ywR$B{(O zcwvCliM%=|$^%b;f8}qxI*n1VJ%uN&{7HKk*`!)mF@`wc5wXk;DT((;b}=s!yLRit zo!V`KgsspHJYAP|f%6b5SG!ogxZb*WSKYeu(|3ICYH70wrcu>YD(-c~U;?NZJ~=B2 z`iOS0i3QsaP*Ui}F?Q4ff6GsgR6f$-eiQh1_ZzfP3;FEw+tJdF+av-puJY)Fi{j1V za=jYjOxdOB65K-B?JjQ%y@+6oUqpW-zwL5s4OOjx!Rs8;FzgQXBhN!R6@jBPn(YpA#=_+-__zLG9shM;B|kx zrGjA7y}@reuDorF%FmWgH%eEr<0U0{NZr4NORYwl!`G93KP?{k!AAw9{up(-p^Qu-74-hJ&Z}#%I%o@00;&+GfM)&H8iyE5%w>u(l$B(IdBSLo4}=~~cXn;pyw`ViC>iORm9=_E&a0%pFoFLv{7HZoeLcTeR`j_#B9n98ZSjdH&+jUC2{SPIR$JI%P4lSNlMR*Y* zMGxeYvVGVOZ-YYbsd?Fss%MuZ*)O0fYO=?=mQp3-S@a@djA`aYn=v`dt}yqhGZw|f zlj&^F$X%{HVp6y}v?wsi2lJzC4jfHB}@zT z#FLS)^tnQckcS;u7p*!fCxOL`OIK0#(@SbxyF1&CXDIe(5lWB}$amQvG~C4JkB3a1 zZmEgai_yq7xKwruS@7zKNR`XrpnLc%M&ULGRw$~8`irGq0aYs6V6F=?r->&c#`1+_sip4UsL|)GeDt-r!ykcx6cw+Q4-W)0x*x`j z>Q_aHPKUMWZ{InCbBd3sR=<+;W$Qhq6YHa?60?tP6$ssx*b4*fIi=q;+8)`rC-KPY z9l3;amV{L9B$Z`vTgb;IvET#_V_RJVj7hDIF@X~{;TLyBe&yTv*3=RdOE-|y`l5;j z*S7jK1mT9#y!Cx!DoxS!TybJVoYBf5*X=3MjjRV6?^-iLKtPQ796M##k|#}qIblU^ zu`}Dq-(dQQqDBMD`?j`}_8LXZML%4L5q9Yc)Eew3;@3W945X?{cmJY%uht;v;z(^a zFIr1VUF=&$(9kJH&mY_x;x>XMd|gwhy4EHSE#pnN*MR=w0rtCkAKPRo)QGrGv)tP# zx9n}#z9B;6lxYM~u8*&?v{)xS!o)`$3oh8gmKQG5VKvw?o?H9#38al0&PlJmc`cRP z7+%Ud$sCyMJoprDfEIlznR7@QKf&7k3G1b~AVItLBVomWbGqXuo|n{ZI^l!#x$7&W zG5F&!aFXW!$9!aCHtrEyjp*Hx@>+Jfk8M&}%TWXI#^yk#~4*NAS77W|w}O z5N}QjuF(3$Emfwu=jCGWtK-En{Z1EJg~;a8g1_s&C84k+aJxBC-CrA^GqOgF3anXb zoLnVquo)G$Wr$6V|#uNsP7Jm`y%-B-(LOYk!Jbm={Ppib`|ZA~ySv@PwmQ@mTV zRN2vhku14er{8wyXnJtz_wwAAx?vhy!B%Fm+_lt^p6^5F9U@FOw=Cx749P9Y2MaeN z-bUw)-=wyY<#;HIr^i<8f6(U>+A~f$N9Dm&C?@Ohac z#I1j3c19OeQyMbhA8#kfT0h~EuG4(i9%GfcMHZ4&_JTe|G$7M8h&9Dvv6t?7V=1c( z<4teYqZ&5&S9;|ro|e;1%5N$?UmGM#>=%-k?Iy2n`lEG{+P%)+vMh{`AZilw!2V4< z*pm6&pcAwkz6*h=rUvn#HwNbu;|(aIhOG6$W#{>l(ShT77N40aR<{b#-!3-Sk6 zk8QPREk2NR*!(Qv$|!WH?fk8An_j16&x1w}XTpctvo?L<)&1phushQJkDboo1dcY^ zI*X2<@=6X)E>-ke#E~O5oH_1V1JA8xet$TKYjn#O-!gxFK-e=*j}{-H#qrIapP_eS z?0Xc$ZZhpb;gRunf&2Solk6T}dyL&xO9%$EsIA&JGsM^4Q1_>5JcQsmLd6GRqp=YR zn5@+V-C}FvY`BBx9ZQ{1eLfuLM5VoVI?OyEMxiHq^A4j z+=E$f*bVB&3&;kRaRyrQshK7<3ehTs+762m-kj6!nWRH&o|Ik?}CVe0{{Eggwgc!MeSi~4)6$(aBi57#zH zWm)wYE;N|VW~~qE1tUj8Z(bc&T}eB{y*-*uZn#ZwW)nA3>JY9b{U+gLMRoAO`-Ah3 z4GIi9{L@Gy$G;OQII!AV`;lKYceWk{p1J0^^I^kZpAE%o+E9$PeuNl4VLANieGWLE z>pk(&$wk`bdwCW0g_5-gDM_1~tkj(g(FshXYKiKRX{~iC)3OC|9BQ1^yUf8kRk>f< zIpo}yyyrLANHA~Nkncd?`rz_=n7AgV_EqsGDa^Rw>=fh&0IJ10Ly|iA$X*QEt#-=v@uykI?OyDbj{o& ztbAE`Vnrgg?=48|EB6>#?|Ht;70tIii`I5ZR6BB*`tajjwTQ(3DgtCYe$ zDz;p!K*ePL46cQ-K$9Rh_Ch{&b&y9I6Ad)BCiaplC%HWqq@bwT^Ykekos0%Ecu?EQm%kkhCy`WLE!}M0ApiB7i zy}AT2&yt%CI&PgZkV~UL^9b?@WOy#C^EIB;+}<7zGEgj0BQy5hWMv|GSvP07!OBJe zf%DU64^B-R6}I+!Tch7({F!UC{gp^eI{nx(116p<&|a3;Co$z5f}Kx{e#agpa-mq- zQ~WgC_)TdpnK77RIVe`2iiw z`;FM2z*Qs^*AwP)`S+lgCd6%_(m=g_C`co$`VgUSKy7J%2TCkXqR)|#w*9z~1G1VP z``YLEdwqp_1K*O%vUrE39n9N04RKvhMY+w^qn)s&;3L|HOT~3!|7xu`Kf~DOLW)>* z3%S1>EJc_WB~*_s*1*2*&Ig^MXv# zKgs;A14-R~#94I8meGW7+(1}fC;E~4UkN}AQ zYFV9j-Q0jxkaRVj>)-_h`A;QWwFj>CD3P^Vri1tLL2=*7g^}+m;FO3XPy|uI6CZpb zeJ@(}Xm~f}b&wcV0<`=oM8XL%KLy}-s$PdLe%9vp;gmJm1jE2 z$#@r6FSO#&k10ic1U#XtV8weeBJAhKeBJhOzI`|?_V6H_ZmRP31CLanOxu(O`R#Gn z6yG-`)AzwX&gY~g>Y(45WALh5BtXcm$!jnwquYL?A1I1{$^s8{9;ADV4!P6f4c_H|=I77za3 zb}2T5)ul518s(4;-y$VGGGJV)$je!Kbg0a0X(5z@*>-NJsLB5hn@eSmG_(47D+Rdy zlTnuFLJH$_2tHD;l73r6N%AF(B`&C*|HqmO7hQ-l6jO@k!#qG|?-hJywO}}(N(_bl11G-HYbMdZs zkKpp!w3ZMm%OQfv^ssUzxOb_sHakXioSnEwZYgxA%r%&VDxta#rvZ_Zp5va;QxG`` zko+iOWp)YCWJj^ucBnX3p0XKNdaBbcW~zt}Y3TGxh*-6p1;-s29+Ke-WSYr$GPL9` zd5$xIiN0%T`69VhugzE{FEo6bcxt;&=@;~&V07iMeD<=fRVt>$`D&azbXb3dSwvuS z4!-T&V1=QG1iGheMJPoTf)*F=wyo(buT&YNVu6t^od?DheNK#U^Pp=Y1Y!i_xx{)hC_;M?+s6ht9#1O{uH=sNrk_wOnWcanf#&K z&GdPdt~ZbM-TY=1NP1O9(zqTIugo@#v<D3+YQl@5z$PhV+if>Qqf0}Nn}?&^}@16-c_l^UBh9Q z^>b5-vi8oJ0xJW@!QVdNZ|w zC2F+G@Rp>cn$NSlhgM%7Nu=f9qW?0EVU*U=7jjXS0*#(w-+TNdIu%zZ8z_i(snNJ zEbJQKq391)*Sa`>Ykoqa!cY^O_?jpm1W*){DWf!>&}SOMGRcBi7A7n8e} zud2fjRl6&NRbFWMKf&yOxKQz##4X(AxlkN5sg8@cwgvv#y!?{uhR+tN6Cv$q zJ3Vt^aYvtIWs=pwN4@l1TJ)LvMV;SnE3V!%mhuwlYD_bPG<^lPhF&Nk7vGpDzTVp%$9dc9f)nG$TGeiW zQPyW_6F<8oxx)A=e%9+tC+F*SaA&N4G(MbchvG@kr!WXC}aUZwWW&vBi~m$2tf z(Qx{vlo{C$PsUM?=^oiFuZJI|;P&5}N!`Y19?BO57xa+w9~@Z7Ms^Ja3~S3;mxmTy z?|4om$h3SDH0>+_NlDs|m>Q6Do&`e4IecxzTZ6lap~pg^Y4IyTZXw0dlXZCnE} z(|zi=FX>L>{QP55So1kpK51K|=~nR<*re+WtmYKnRJK1zQ6Pj{)=Dv_CslO*ncvdN z_dtUS`TqhW_mA2X!-YZwGcm2e$ z7gVOreC;*t5Cbdv(ZaBtudujia6Cr6C8QmATpd=FlO^cI{DV;*U9IWAOwu*JDq%kG zcsU~ms+A}7;wZd;s;6Qh$Ik;^`+$THv`6t^rbqRuim2P3j`+#4t1lyb6qiBA&*}R% zs=9q8pVcB|g$y}EGI3Kr#j}HLRnPrh?7?M`UJic&MV>I?2yDR3#z+Y@{ygD(A+A&N z%`L%BeTS#x90~h|DhI=NX>ne3HNo47smGH!d?ebC;e8TEB}+JWyK#)-O2#JPZZh(V z{L2TE((}{w(el|CyYC*2wd}-yuRnHiux=m(Rf9U+-l%V8fZ1lQ!dt+oArK zI=tkAXquop^n0wpPJ=?2^SGt^d=3ke;W{ z%%+?mNVuTc?-o7S%*Jr<$TR7Tr5=wDZNoi3i?+rLAqcV}fr1acI=pFrZ=vl0UPM)5 zFx^@4j;b zsUTL@grL|3FIHi&1sd!yAuS%Mafog|ZN~4o=BX<4T0P3z?~$1%!`@p8p&P7?F{_Q~ zJZDBn(=e5UrEeG_yAJc}`oS=!YOG@4ZNt~NHCmGCX1_|wcJ65Mj#*)W2y=P~385k( z{|Y>dw*?4}3s z6Q?Em)UKNERW-eQAWtGd#|xCf>TWdYod20~J3SW5E>K^kzkQy8S{BUAjR=qTtgtUV*8?XpJ!Ss* z+G91|tdHFC$Np*Wn4P7)=Tkq?T2$MCbm;(=PjzG$7%F!Dc!HJv{dE*+(a6};dBL4r&GKyNG=7$lc>1Q8wj9j2#t$vVo|7M% z9AIBw5QZO1^rrVJlrJ}Gu-FFb>M@ptlj$VyYflqM^u7<%<|6Pmp3C71P5ie426nfb zO7?cDMvdjsT8B#9Js!o?MP5l0HZ$p!^DM4v7p>mK#a48BuGB7NiGF0qi_;I<&W=BK zXYKK+R-esR5dyKR%rUG+tZj(+?O2?YvsOQ`TkkZ)Dm8#rTlJh z+Fj+DY`PNi)%96!O6XvMDtAjJ3y$?DPJR4Dn(D6&%HU(uh-u^vGs{xa@ADGy%1z#u z-C&{ufg7sK-&Gfq`6e{xY0bgF`O_XlwW+?ihG`>O7m zW+R4R?-JP2launrX!u;8b(a*b_pX|xKbWrFw+NyIcx_28bU!uM(Jrz`C9j5&*dBD-kOkMUD33pmD`@?_)RdO zFx=rZ-NjH-X%-@);`!MgDe9)r0 zK{o`SDW&i-iIF#WfZ& z(v<1=4DE%23{Y6g(%88#|5y{peo(yI{HT)F!fg79`Q_XD8SPlJO8AuYVRJGcbyU~R zSN%I1Lx=1852KJr@6Tx?#=&Qo^_PnEZo3d-cC6k;am&QSx_;^Mg2T82=BxZGf{5a? z>1I(fmqKv^E6!N{y?W~CBAfoIvDMsWx%{52SwhmObpQ^>)+<*(d}Vu4wnrlQezNz#f_0_eyrufy_O;te-qeSk<%-?gcn z-m8XKc`hahZgg^ccJiN0Ss>hkK~SS%OH8l9Jj+#Dw3a-M4`Z;xzp7JdIJI`ceUZGs zQQk~bxyk>9vf|XCBeTiEHQQ=U(Z9mh>=qEs2KI8jLnH0 z7iZHmHFEX1yWxL(qOz?xsL(u%KZfB$WO}#!>4YbI=t6%67v!vw_>er-qFX+H{VVr* zt2+-hF*8i9fsRr+_EOuj8|h%;w@JpfK-a!0F1KlL-OtB={r_t3EZ?H)-aW23K7vI^ zC|!~w3JL-eB11|@r-Y<1AOb^!fTA=5(kUW?(%mI3oihyGAVbGU&ROFxp6C1n=hc}v z`?~hEXYIA_6??^fe?Lu*-5HVOb|hF&I6n7b&$)IrO#3|#(YS-6h{MnxSGNm@)Bag2 zI+)=euL}M!2z!|1k|OeJK!FU;d&zSdthL_PhzhU}e~JfQ3++=G>Q1LfN#9f;*^B8l z0|zJ5{#((F<&2M_yQ#l?$Ft{8(($0Io#r{i^AqV?BpUei#U3|tw3k;-XtJWMH1A$J z`0{jJ>0$wIgaAyCu~}6^<=}%jDoy6V)^%m0qN;&YLeH}{QdE=jIAPcKMs^*_zR{YT~22E#&&$74$bw#Qg3bFKv9fRpcM|O{8EOl_>JEo!M4_L`zv$Po+mLsUDjfJ zd~z&nR;4#~V3k(T)n5N{Xd|sEvCOrB-%tGD!0GROze?aldQ4+{rtNds^R`(<)!p}l z_;>5=byrKv4@={=gZY+SX-4x^veD)34pUX9Zz1<`3kj3Kb5jmOJdRNK9Fga5DZyCf za9DNXJvtCA$qF_Fs3FgyFMlkDNj~*bhc2)M=`ZS7&Bn>Xxr7aEcoi^3H@I2X9a+>N zo&%Xf%5riaRD;bXrDU;biz zOn`VeI2kvjFQgzMBbJ#-RbEpyx;z}ISVW>bcTgJr_!v=Wl=?;m&fCW6&)_*`KJ-L6 z_~SHREbQ{8=dxPa`7}$`aYY)(Llmz^o(HMV$#EIIbhk0`VwQZkDb zR^*k*x3hV$kvP7bBu6SKKy{Cxu+K5QYS33}O+6-pcI*v9*p&9|izu_Hh;?6;oD}0U z_;I+p>+=%Z6Tzk;*)8a6FtGM&0}k<3GKr{}uU()K5C?TjW=-x_Bgf^>Fh7PGVZY`{ z#FCbx>{&9>S8%@$o|A05u0`4T!e^@0$f0#!Y+>(KY~=wj1uDYpL@>N!J`BY$@ZGs; zcSuRUba05SJYEE?;+MiG=Fv-XUUHVc<*}GJyS!0@C}!bfWBAj^axkDz5j$VO5;2V< ztf?JEll_T}Q&w2c+rj*=BD|+;D~yy2COL#CNA=^VSPH%nMR!-{FTQW=9j9t+XM`4R zCOUg0>4qdIkVSTk`kHoKU^-59-#>6$b^u49G(bygKm#l>JMM!5g|12*G<5a;ddcnv zBku^76GXuH*@Cda%=t297HOybx1c}vt8p7Q@^G}!oRqG?Tenr6Kx8Vt@zvxkmn&H3 z4X)-1mMr)74-92z430?qjwV2Vx#T5MyHF-0T+v7}2~Z z+0E~Ut3y_qKV{t~`*fUYG#I{P4$)SzmV#=5Unqqx0dLPQPP4Z-lXh`W9XAAR=mh6N zNl}pG^`3AFl!635h`k+Kh@W){`3&X;pGfX7<`cs`L@G{*=r(B%J|olY0+TlyxbOem z$tGTNcFH?F32zLaE21v_CR3M-cp6sgyviIl5ySfYfTE3ebKJM{0uLY zQ6a>g>ZAU!LKN%&DSH$)yyL{{JKuS>vfeJP1QE;K)K_mXFga#mn|~o)p5UVtg5!nE z%*O?UHQIN|r91%}SD%QvpqbU*dgR==GSxTkg(xvfa}UoAqTEfNqs_Jz~(5@mA;f>q9o83|HQvw&Z=pflook{3F}jcfz^MOTPw7Sj%;{sO+I+eqi-tD5}8Mj~Ui8`V;c&6fvS3`s$> z7Yh&#U{d+B(s*Jr+n4{a~LVXUYgwu@TBooIly|2=xmrPu**h!<_+!GlwW=P$|7x4g2WZ%Pv#ha0K%+TnO<(%%@r z1<{MEcUjR2Uhq{yfd3`Givbl2PbePCvz}g3&{gb-H_{<%>TTz22%-X_s#m6R9o#;# z3ItIi{*JKsXEE4;MnPAE*7$Ri!-3f5L6x059~fzqnz&gv*g0RIINslG0ej!BMiV(%wuvH)#xDnDgEDIo~RL!cA(Ihg#zunI-TpDz$YSkmdSlQcb7)noo3 z7cl;fDFNlySH$iY--K}FvxEMST6$6F8w&fplQqUb1YHx&I!KNB(?$G&CeNjLnLKmw zg^LCVRDK7E(~K(EL{Q8wofwcH*!pd|Y`H7pWtLA022T$El%q>Y=Rx_qf!Kp99G*ys zJ+=`ATfZ|8AFCUXMr%tT{Uz90V%VU~}FcK26t!043C}H21V? z@Eptp#wt>F_0fxy`hWj3$!d~&wu92EDI`zIg;E@>@WM&0r{&J&cEcSlZVvs zjW+qd@SK$2QzMMSz=nhhWR863C!O`t{2F5WpLb0N5_r`Ayc5X`8o&R&)qg0R19riG zl?4Bl8U;achm6ArEaU$q{^vb@|CL((^G+#$Ado}yBl92X@aGBYY5&$&{8!H8BY~jJ zlX&NU$giIxXbk-`T_O^Kk}z>J2#x-`I6zYG+JA~hxXt_o^j(PQ;6Ehm&k-bx{{9De z>R)n;&#M2S3ICQLEc=gi&-p=s{LhVd3OaumjEJCi_#chHe;M=tjr4zOBy}1zZqhZa zYP*e&FIp~))nG|KgQyaoog9t#KP(6l0Zb?9NI-7#D$%QTE83s>AhE~NSG8700PNJB9j#?q^`$PoyP;yedIGrJrcUc+ zC_-o7B~Ty$$48W}>_ZcnBFs_Dc#3*X%buq8P7S_hI|CS`lG64_mZ#lT61xen!>(wH zx%i!aQ}zC72mmnDLL^^kjJ*bO0T1b7b9~xr?a{BC*tfA{Fii>kVP8@KfXrmcMKF%M znm$<)?m^WbJQVcUvo103j5>Mty^DgRSJn4u$PysQgm|@cvx80R>gL16v>n(d0#)Ph zHB}F&9sbm*H+!J!`rNmgIU5UrNU}6RTu|vV0=0(#%c2#2bvU`{qf>Xws0N-l_PY7n z7dZ6K@$xm2E%B6fqM%*zl)SIAa$=`@Lp_JfX~wve-CzY;S3^<&%pBf<1MLO03FgRp_lNg6j@0R=@v<30m>5v#tpQj4S>rOi!SRTpr(vmC$%arm)@nC z)czN!=XWc=tUfouI%OUXMEICXZ26JuHn7U@5o5C#tbuIDJC;VA^G*HjwYJmLaEB+= z;=f6-ne}u-WfEgk8dT^wBX~lBsLQLcRwn%K_MSeUgA#MACl1u-k!a0K9d1(ZqrGpR zcTaZv3=mpSJ+1tPGY_W@mJho4>2RXsWT)PuL8vBs=`{Wfhw9crc;$&$kzWwghvAQD z>bEZLo^JY1d$=F3S0n*m5>#JH!3a2JBesT!!IDhj?dED^qnUQZu{eX%@8P5Z!>YlR zELgxbp?gX8)iy4*AeS0xQvMe*c?#FlmT+TMOoFdxq;?5NF9D-1VT|>c#-aW`ZtvrR z{oI0Vi~jm4k2SuMS0g+*^~q_n&tHrHsF&4Zw?yFLqzQgGPr{w*#-z}W4e$_3D zZx-$JX-)YY!%8-)*G6mcz+5QnDaDa3gOFEbd#pQY_>=a>pk-z06ui*xt*QAzIIHj|@}$!+F*2>^Q;XjCpG+TLkb>i#CbMem6%av1_;+wC6FF^0jXy3y=_UWisrTpdCt0l@J{LdF9Yl5PTn z?q?1>me8uTr|NaTb8+h1%4O&zabW4~>Nr1R0N*Smsh&BxK3WQ;{1zKS`bo#Ti?2eL z?$M%T4VsQ_Uw`+x=JZ`(0JQ5S_5J;{OSqR+Y+D++gmikC|4q7xh8XA5^7vp(jlSg8?Mc0H0{+^lxTREbfjVTf&z z_bcCrv4+7@i+06>!U@Q(k(JbqQuc)c;22<>6>P_ITZ&Z~`a|uovL_f4*Ag8pgY@2= z8By6q=nix9z{kM1Avf7NNUp-IWKF&i4@gFZ8OdS6?=nwK2R|(08t~qNyW-Qh8)#DT zy~r5h&DM#H6#|8>hvYt+Y-r&Ll8)HhdZN|Gn46Izj%O!M;$-@C!&Q9!hFp*Z5@4_= z-E6$5toHQGG&zyk69DNpiizZk`8dLV5x!@%rXbA41HKPMBCDCvxp>zzo|#LUG!@(P z?FPdB8=915j!S0vJ*4Erw37u+Z*QlS&mZ)6`+LGQOq8<4-q(}_7@zILjTp1Bill(f zBxM6e^vU%j_+e^nD~4?k(Eb2~O!4LFu`Phd6^st_d^)gpU*%ax72_ulQz)RwN~gjA zFJajPRWUDX4RUlak9~0`N9;}YOjU9_s>$19W;$lD5WezkQ**JlGdY-$22a4C=x~I_ z41>$?^Ci$U%T=P(-aYr1fwnhei@WZQ;%x~Z-vzU1< zyv~69WT@Awl5ufH7fhFE$mE=`aqs}jy@nFH?dm3K1jB-G71 z4P-z^R!T-AD!<8hY!o(Mm*sTjI?AhB2bjT8$G6qDG=`0;rbNF%7Xk9JWJ<&P@X7qD ziXm)IDXfhV5c@1@QUutK%g&~br*QgbqphtHHgdwY6c`n1TDi0fBSQsz)hs6~a+0sY z3d(RRUm|38gu}q1Vh2nns>K_S`@DH|DC>sPgOYkq7i_+e>aV!ou@WnT5dG=pBMU?5 z^!(4OYUv7#V00*dZjk49g}=6t7oPgV1b70g45(XbOz>ZrlWYDXAJ^WpNNuPv)w|t;az0NxgqV~01 zLSHw>RJ~r9@WxG|=DxSisdTD2tA!|JOj0uj3n}g?)}pJ5%U?43o;dRM$-`)abjLog zl%y>0gP7#$8ue3cbegG-cT zY#jLE;7sLtDg$}O_xEfAgGaOPzBp=qBFullPwJuSZF`L?YF*->Pt|1nG38jfQ&-B> zR$Y94?L+AI6e0r=y8}$c_@g0Z%BE3e5`u89lp4X3(XZk*SHbu6r(OA&W$f%xZ=tV( z%h3dPR*czqW5qHWMFea)mDh^SO7yF7uspD%tchHTYX8GC{QBgRd`dYoh?;Q#!zGix zZ;yyjQfAFyJNY6R{kic3Pyc!;~TWy%OpjV-iE+a=Nq@Q@fyg?Vz5~9(4!xaVUi_uJl$D zwH}hZVKgFA``y7BsvxeQKF$PX{Zl*2GLxCD>A;(%+`S+*!a2ZPA*V%i0qxl)om0cq z)OWwUaAi>v40?!{k&L$y`_%q0(}c+zHMbBPMs!~k3#}lScym;sleP<`#?}CJ74!vsK}k&?Xq*KOAEJ}>OUYSo)#`HtXHLKrDB^& z$|21%*%i5SG8Z0_tD5uhBCmGa(Rq_Lh1WK@A~NMEha|^Wk2u`>+^rxr6!h%D7g}}m zt2t=G@6uMC|MjyU9u8yRgLrmX%rpA0drb(~?N$oK`mszH2%Bk_PSFG{V+7dH4;DWz zSKj=-9J9C5R3Ju*B8HE|e=tE^>e+bj6xQ>KK2Xq~uR1wXPwcu6*N>$lNNl0LP= zX{h2iiQar~%(T9av^21hz$asoD2|ze`baHLF$(N=Iq7_PBFlFLn0XjE(jz~(a{wYn z%NyT&`P;B#HUgJ;PZYR8_XVSC!HNSpoBL>#`1E0C_(k317t)2R5qxf?2}|g;kKYzr zZZ#eFP+vGg?5w^t3!Lxx^ysHdMd-%}0~2RkYOS`@(_@_EA>tUZJOBDmISn2ue%kj= zWt#7Yc*dmlZ&`%#Lq+E+~helOif1O^KZdkKP zdek~Y)(KZlwagJdy)ytYxKGg;SRNKBZT<)|hjG{pS@`f)LYt@VD6`j4y~72Kv0-ON z48Kd)W?I?%B5Lx^L}#__j%g`o2}W*rSCIW?nK%CI)Q=_`$CJ=kL~T-3zpI~lziiMV zJkPu8y~(j5A7pR7J>pWhkm0Kga@CePLUn!y)oH>9@ko8)nwpPT(rpQMHICBZhHTv& zj1XOddq#7umiU{q7a~H>q9LA}`?)(U46=o{DZYz-Y=luoeM=wa`;HD}P$(qNkr$0* z&J5-aN(wrQN4+jBiy~TeQMfJ7Id}NXlG)vU)H=2*dYDzv-VMSZMWgLN&t(};uUxo4 z09Tt3IO)k!la}$ShK7$_mi-|}F{THp`Duc_P_!PBR^sS%M)_p+VM_6SkkHyCfxe1p zq3&k7yi(k&&j~dAFAPhE^o=NQDWwI4^;0*&H}uC|=l+a$%&nnwSEjDo&9d;D**pp` zpXKmi&`EecZ)BwNXk>x0bfyU1Z(VQP;Ny+ht`yRaYEw~a`TSp#FaVRB<-nVM?mD{| zC{euJkP15LpFyy*Q$Fa-e01`Olss+C(clbi!!e_IOWcK^9NGWYVC)!RhF^Q z+;Ght-XG24&oTt=z5)KHTu-P;dRHY7ZxfD-IMN?9y;$TdJsu;@4Up;^TI(AM9-k$> zZ!h&}{YCVd`LfZBoLW)urvJxB+WJdFjy`8b5KmkC2~Tm}$1WepvFOaI723`TqVvxc zp2v0&tCw2DF6ib6C`N@^z3FA@ap0q6OSs;t64Oo}LeHY6y}LkB#SJajC|oRR%4t!P z8CG=ECTnwLdE-ted?c|W|FpF);;1VMbZ657vZ&g)7W%zxb zHE#h%^35UB zR*8&Ih~L9?V+MY7R)${ZuOAOi>m6r(kCk)s4Zf<&zQVg+p?zDR6wPCOYq!XK@{~!e zL4U?-?;UPT-*5%vGUYVAR}?12wlyuAzP!4>Y~#xHCX6m~&Z*9}CvB->*e`pN#Eayr zCC&JKxa~SU=Z~JjM>Tn#Zi;RbTxqpJ!W(s|C6al-UV_HiF)7jNQHU#K>EhAAR0D>e-yQZ1}#tL;c{udvp0wqWqi8q|t~I;zcjk={A~j zs!JX&sEtH9-rLK?@}VbzR;%6iHV$dWPM}!*#JPc>@^L*w{!;9WkL2DI%9LACSIpO$ zT&E!R2GCiHvz^1&0eXvbY@1*6sFovvQd&8oiFOqHLjcXQSr$eM5>t2 z9B594_zmrUW<`#yC)wc)I^&<4-b>xUYFv|ZCCrTen?qR>7m#j&g#RS| z-+BJ`MT4aua;(`rkYe+XhttV*8T>fVApRQH@V`=-(IkG*Br)>Z+3N3<2`12%n^CRD Wo Date: Thu, 29 Sep 2022 19:15:12 +0000 Subject: [PATCH 2/9] fixed pattern prop name --- .../@aws-solutions-constructs/aws-lambda-opensearch/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/README.md b/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/README.md index 72d835556..8370a5b3e 100755 --- a/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/README.md @@ -126,7 +126,7 @@ new LambdaToOpenSearch(this, "sample", |userPoolClient|[`cognito.UserPoolClient`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_cognito.UserPoolClient.html)|Returns an instance of `cognito.UserPoolClient` created by the construct| |identityPool|[`cognito.CfnIdentityPool`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_cognito.CfnIdentityPool.html)|Returns an instance of `cognito.CfnIdentityPool` created by the construct| |opensearchDomain|[`opensearchservice.CfnDomain`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_opensearchservice.CfnDomain.html)|Returns an instance of `opensearch.CfnDomain` created by the construct| -|opensearchDomain|[`iam.Role`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_iam.Role.html)|Returns an instance of `iam.Role` created by the construct for opensearch.CfnDomain| +|opensearchRole|[`iam.Role`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_iam.Role.html)|Returns an instance of `iam.Role` created by the construct for opensearch.CfnDomain| |cloudwatchAlarms?|[`cloudwatch.Alarm[]`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_cloudwatch.Alarm.html)|Returns a list of `cloudwatch.Alarm` created by the construct| |vpc?|[`ec2.IVpc`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ec2.IVpc.html)|Returns an interface on the VPC used by the pattern (if any). This may be a VPC created by the pattern or the VPC supplied to the pattern constructor.| From e60d4fbf9afe6a4ebcbceff122ea012a5c8a12be Mon Sep 17 00:00:00 2001 From: mickychetta Date: Thu, 29 Sep 2022 22:05:31 +0000 Subject: [PATCH 3/9] fixed service name in README --- .../aws-lambda-elasticsearch-kibana/README.md | 32 +++++++++---------- .../aws-lambda-opensearch/README.md | 28 ++++++++-------- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-elasticsearch-kibana/README.md b/source/patterns/@aws-solutions-constructs/aws-lambda-elasticsearch-kibana/README.md index d04c3b1f2..8fcc243ec 100755 --- a/source/patterns/@aws-solutions-constructs/aws-lambda-elasticsearch-kibana/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-elasticsearch-kibana/README.md @@ -23,7 +23,7 @@ |![Java Logo](https://docs.aws.amazon.com/cdk/api/latest/img/java32.png) Java|`software.amazon.awsconstructs.services.lambdaelasticsearchkibana`| ## Overview -This AWS Solutions Construct implements the AWS Lambda function and Amazon Elasticsearch Service with the least privileged permissions. +This AWS Solutions Construct implements an AWS Lambda function and Amazon Elasticsearch Service with the least privileged permissions. **Some cluster configurations (e.g VPC access) require the existence of the `AWSServiceRoleForAmazonElasticsearchService` Service-Linked Role in your account.** @@ -111,7 +111,7 @@ new LambdaToElasticSearchAndKibana(this, "sample", |esDomainProps?|[`elasticsearch.CfnDomainProps`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_elasticsearch.CfnDomainProps.html)|Optional user provided props to override the default props for the Elasticsearch Service| |domainName|`string`|Domain name for the Cognito and the Elasticsearch Service| |cognitoDomainName?|`string`|Optional Cognito Domain Name, if provided it will be used for Cognito Domain, and domainName will be used for the Elasticsearch Domain| -|createCloudWatchAlarms|`boolean`|Whether to create recommended CloudWatch alarms| +|createCloudWatchAlarms?|`boolean`|Whether to create recommended CloudWatch alarms| |domainEndpointEnvironmentVariableName?|`string`|Optional Name for the ElasticSearch domain endpoint environment variable set for the Lambda function.| |existingVpc?|[`ec2.IVpc`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ec2.IVpc.html)|An optional, existing VPC into which this pattern should be deployed. When deployed in a VPC, the Lambda function will use ENIs in the VPC to access network resources. If an existing VPC is provided, the `deployVpc` property cannot be `true`. This uses `ec2.IVpc` to allow clients to supply VPCs that exist outside the stack using the [`ec2.Vpc.fromLookup()`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ec2.Vpc.html#static-fromwbrlookupscope-id-options) method.| |vpcProps?|[`ec2.VpcProps`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ec2.VpcProps.html)|Optional user provided properties to override the default properties for the new VPC. `enableDnsHostnames`, `enableDnsSupport`, `natGateways` and `subnetConfiguration` are set by the pattern, so any values for those properties supplied here will be overrriden. If `deployVpc` is not `true` then this property will be ignored.| @@ -121,13 +121,13 @@ new LambdaToElasticSearchAndKibana(this, "sample", | **Name** | **Type** | **Description** | |:-------------|:----------------|-----------------| -|lambdaFunction|[`lambda.Function`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_lambda.Function.html)|Returns an instance of lambda.Function created by the construct| -|userPool|[`cognito.UserPool`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_cognito.UserPool.html)|Returns an instance of cognito.UserPool created by the construct| -|userPoolClient|[`cognito.UserPoolClient`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_cognito.UserPoolClient.html)|Returns an instance of cognito.UserPoolClient created by the construct| -|identityPool|[`cognito.CfnIdentityPool`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_cognito.CfnIdentityPool.html)|Returns an instance of cognito.CfnIdentityPool created by the construct| -|elasticsearchDomain|[`elasticsearch.CfnDomain`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_elasticsearch.CfnDomain.html)|Returns an instance of elasticsearch.CfnDomain created by the construct| -|elasticsearchDomain|[`iam.Role`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_iam.Role.html)|Returns an instance of iam.Role created by the construct for elasticsearch.CfnDomain| -|cloudwatchAlarms?|[`cloudwatch.Alarm[]`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_cloudwatch.Alarm.html)|Returns a list of cloudwatch.Alarm created by the construct| +|lambdaFunction|[`lambda.Function`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_lambda.Function.html)|Returns an instance of `lambda.Function` created by the construct| +|userPool|[`cognito.UserPool`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_cognito.UserPool.html)|Returns an instance of `cognito.UserPool` created by the construct| +|userPoolClient|[`cognito.UserPoolClient`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_cognito.UserPoolClient.html)|Returns an instance of `cognito.UserPoolClient` created by the construct| +|identityPool|[`cognito.CfnIdentityPool`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_cognito.CfnIdentityPool.html)|Returns an instance of `cognito.CfnIdentityPool` created by the construct| +|elasticsearchDomain|[`elasticsearch.CfnDomain`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_elasticsearch.CfnDomain.html)|Returns an instance of `elasticsearch.CfnDomain` created by the construct| +|elasticsearchRole|[`iam.Role`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_iam.Role.html)|Returns an instance of `iam.Role` created by the construct for `elasticsearch.CfnDomain`| +|cloudwatchAlarms?|[`cloudwatch.Alarm[]`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_cloudwatch.Alarm.html)|Returns a list of `cloudwatch.Alarm` created by the construct| |vpc?|[`ec2.IVpc`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ec2.IVpc.html)|Returns an interface on the VPC used by the pattern (if any). This may be a VPC created by the pattern or the VPC supplied to the pattern constructor.| ## Lambda Function @@ -136,26 +136,26 @@ This pattern requires a lambda function that can post data into the Elasticsearc ## Default settings -Out of the box implementation of the Construct without any override will set the following defaults: +Out of the box implementation of the Construct without any overrides will set the following defaults: ### AWS Lambda Function * Configure limited privilege access IAM role for Lambda function -* Enable reusing connections with Keep-Alive for NodeJs Lambda function +* Enable reusing connections with Keep-Alive for Node.js Lambda function * Enable X-Ray Tracing * Set Environment Variables * (default) DOMAIN_ENDPOINT - * AWS_NODEJS_CONNECTION_REUSE_ENABLED (for Node 10.x and higher functions) + * AWS_NODEJS_CONNECTION_REUSE_ENABLED ### Amazon Cognito * Set password policy for User Pools * Enforce the advanced security mode for User Pools ### Amazon Elasticsearch Service -* Deploy best practices CloudWatch Alarms for the Elasticsearch Domain +* Deploy best practices CloudWatch Alarms for the Elasticsearch Service domain * Secure the Kibana dashboard access with Cognito User Pools -* Enable server-side encryption for Elasticsearch Domain using AWS managed KMS Key -* Enable node-to-node encryption for Elasticsearch Domain -* Configure the cluster for the Amazon ES domain +* Enable server-side encryption for the Elasticsearch Service domain using AWS managed KMS Key +* Enable node-to-node encryption for the Elasticsearch Service domain +* Configure the cluster for the Elasticsearch Service domain ## Architecture ![Architecture Diagram](architecture.png) diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/README.md b/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/README.md index 8370a5b3e..830fca8ff 100755 --- a/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/README.md @@ -23,7 +23,7 @@ |![Java Logo](https://docs.aws.amazon.com/cdk/api/latest/img/java32.png) Java|`software.amazon.awsconstructs.services.lambdaopensearch`| ## Overview -This AWS Solutions Construct implements the AWS Lambda function and Amazon OpenSearch Service with the least privileged permissions. +This AWS Solutions Construct implements an AWS Lambda function and Amazon OpenSearch Service with the least privileged permissions. **Some cluster configurations (e.g VPC access) require the existence of the `AWSServiceRoleForAmazonOpenSearchService` Service-Linked Role in your account.** @@ -49,7 +49,7 @@ const lambdaProps: lambda.FunctionProps = { new LambdaToOpenSearch(this, 'sample', { lambdaFunctionProps: lambdaProps, - domainName: 'testdomain', + openSearchDomainName: 'testdomain', // TODO: Ensure the Cognito domain name is globally unique cognitoDomainName: 'globallyuniquedomain' + Aws.ACCOUNT_ID }); @@ -73,7 +73,7 @@ lambda_props = _lambda.FunctionProps( LambdaToOpenSearch(self, 'sample', lambda_function_props=lambda_props, - domain_name='testdomain', + open_search_domain_name='testdomain', # TODO: Ensure the Cognito domain name is globally unique cognito_domain_name='globallyuniquedomain' + Aws.ACCOUNT_ID ) @@ -97,7 +97,7 @@ new LambdaToOpenSearch(this, "sample", .code(Code.fromAsset("lambda")) .handler("index.handler") .build()) - .domainName("testdomain") + .openSearchDomainName("testdomain") // TODO: Ensure the Cognito domain name is globally unique .cognitoDomainName("globallyuniquedomain" + Aws.ACCOUNT_ID) .build()); @@ -108,9 +108,9 @@ new LambdaToOpenSearch(this, "sample", |:-------------|:----------------|-----------------| |existingLambdaObj?|[`lambda.Function`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_lambda.Function.html)|Existing instance of Lambda Function object, providing both this and `lambdaFunctionProps` will cause an error.| |lambdaFunctionProps?|[`lambda.FunctionProps`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_lambda.FunctionProps.html)|User provided props to override the default props for the Lambda function.| -|domainProps?|[`opensearchservice.CfnDomainProps`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_opensearchservice.CfnDomainProps.html)|Optional user provided props to override the default props for the OpenSearch Service.| -|domainName|`string`|Domain name for the Cognito and the OpenSearch Service.| -|cognitoDomainName?|`string`|Optional Cognito domain name, if provided it will be used for Cognito domain, and `domainName` will be used for the OpenSearch domain.| +|openSearchDomainProps?|[`opensearchservice.CfnDomainProps`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_opensearchservice.CfnDomainProps.html)|Optional user provided props to override the default props for the OpenSearch Service.| +|openSearchDomainName|`string`|Domain name for the Cognito and the OpenSearch Service.| +|cognitoDomainName?|`string`|Optional Cognito domain name, if provided it will be used for Cognito domain, and `openSearchDomainName` will be used for the OpenSearch Service domain.| |createCloudWatchAlarms?|`boolean`|Whether to create the recommended CloudWatch alarms.| |domainEndpointEnvironmentVariableName?|`string`|Optional name for the OpenSearch domain endpoint environment variable set for the Lambda function.| |existingVpc?|[`ec2.IVpc`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ec2.IVpc.html)|An optional, existing VPC into which this pattern should be deployed. When deployed in a VPC, the Lambda function will use ENIs in the VPC to access network resources. If an existing VPC is provided, the `deployVpc` property cannot be `true`. This uses `ec2.IVpc` to allow clients to supply VPCs that exist outside the stack using the [`ec2.Vpc.fromLookup()`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ec2.Vpc.html#static-fromwbrlookupscope-id-options) method.| @@ -126,7 +126,7 @@ new LambdaToOpenSearch(this, "sample", |userPoolClient|[`cognito.UserPoolClient`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_cognito.UserPoolClient.html)|Returns an instance of `cognito.UserPoolClient` created by the construct| |identityPool|[`cognito.CfnIdentityPool`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_cognito.CfnIdentityPool.html)|Returns an instance of `cognito.CfnIdentityPool` created by the construct| |opensearchDomain|[`opensearchservice.CfnDomain`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_opensearchservice.CfnDomain.html)|Returns an instance of `opensearch.CfnDomain` created by the construct| -|opensearchRole|[`iam.Role`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_iam.Role.html)|Returns an instance of `iam.Role` created by the construct for opensearch.CfnDomain| +|opensearchRole|[`iam.Role`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_iam.Role.html)|Returns an instance of `iam.Role` created by the construct for `opensearch.CfnDomain`| |cloudwatchAlarms?|[`cloudwatch.Alarm[]`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_cloudwatch.Alarm.html)|Returns a list of `cloudwatch.Alarm` created by the construct| |vpc?|[`ec2.IVpc`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ec2.IVpc.html)|Returns an interface on the VPC used by the pattern (if any). This may be a VPC created by the pattern or the VPC supplied to the pattern constructor.| @@ -136,7 +136,7 @@ This pattern requires a lambda function that can post data into the OpenSearch. ## Default settings -Out of the box implementation of the Construct without any override will set the following defaults: +Out of the box implementation of the Construct without any overrides will set the following defaults: ### AWS Lambda Function * Configure limited privilege access IAM role for Lambda function @@ -144,18 +144,18 @@ Out of the box implementation of the Construct without any override will set the * Enable X-Ray Tracing * Set Environment Variables * (default) DOMAIN_ENDPOINT - * AWS_NODEJS_CONNECTION_REUSE_ENABLED (for Node 10.x and higher functions) + * AWS_NODEJS_CONNECTION_REUSE_ENABLED ### Amazon Cognito * Set password policy for User Pools * Enforce the advanced security mode for User Pools ### Amazon OpenSearch Service -* Deploy best practices CloudWatch Alarms for the OpenSearch Domain +* Deploy best practices CloudWatch Alarms for the OpenSearch Service domain * Secure the Kibana dashboard access with Cognito User Pools -* Enable server-side encryption for OpenSearch Domain using AWS managed KMS Key -* Enable node-to-node encryption for OpenSearch Domain -* Configure the cluster for the Amazon OpenSearch domain +* Enable server-side encryption for OpenSearch Service domain using AWS managed KMS Key +* Enable node-to-node encryption for the OpenSearch Service domain +* Configure the cluster for the OpenSearch Service domain ## Architecture ![Architecture Diagram](architecture.png) From 620a2ee2ec4a684e90c7c1fb56d102446eb2dc0c Mon Sep 17 00:00:00 2001 From: mickychetta Date: Thu, 29 Sep 2022 22:12:46 +0000 Subject: [PATCH 4/9] updated prop description --- .../@aws-solutions-constructs/aws-lambda-opensearch/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/README.md b/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/README.md index 830fca8ff..b61404c23 100755 --- a/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/README.md @@ -109,7 +109,7 @@ new LambdaToOpenSearch(this, "sample", |existingLambdaObj?|[`lambda.Function`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_lambda.Function.html)|Existing instance of Lambda Function object, providing both this and `lambdaFunctionProps` will cause an error.| |lambdaFunctionProps?|[`lambda.FunctionProps`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_lambda.FunctionProps.html)|User provided props to override the default props for the Lambda function.| |openSearchDomainProps?|[`opensearchservice.CfnDomainProps`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_opensearchservice.CfnDomainProps.html)|Optional user provided props to override the default props for the OpenSearch Service.| -|openSearchDomainName|`string`|Domain name for the Cognito and the OpenSearch Service.| +|openSearchDomainName|`string`|Domain name for the OpenSearch Service.| |cognitoDomainName?|`string`|Optional Cognito domain name, if provided it will be used for Cognito domain, and `openSearchDomainName` will be used for the OpenSearch Service domain.| |createCloudWatchAlarms?|`boolean`|Whether to create the recommended CloudWatch alarms.| |domainEndpointEnvironmentVariableName?|`string`|Optional name for the OpenSearch domain endpoint environment variable set for the Lambda function.| From fcbfc34cacaffef4eb672729c1ef6a76208b7037 Mon Sep 17 00:00:00 2001 From: mickychetta Date: Fri, 30 Sep 2022 17:01:02 +0000 Subject: [PATCH 5/9] removed legacy elasticsearch role description --- .../aws-lambda-opensearch/README.md | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/README.md b/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/README.md index b61404c23..c3d47f10d 100755 --- a/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/README.md @@ -25,13 +25,6 @@ ## Overview This AWS Solutions Construct implements an AWS Lambda function and Amazon OpenSearch Service with the least privileged permissions. -**Some cluster configurations (e.g VPC access) require the existence of the `AWSServiceRoleForAmazonOpenSearchService` Service-Linked Role in your account.** - -**You will need to create the service-linked role using the AWS CLI once in any account using this construct (it may have already been executed to support other stacks):** -``` -aws iam create-service-linked-role --aws-service-name es.amazonaws.com -``` - Here is a minimal deployable pattern definition: Typescript @@ -110,7 +103,7 @@ new LambdaToOpenSearch(this, "sample", |lambdaFunctionProps?|[`lambda.FunctionProps`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_lambda.FunctionProps.html)|User provided props to override the default props for the Lambda function.| |openSearchDomainProps?|[`opensearchservice.CfnDomainProps`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_opensearchservice.CfnDomainProps.html)|Optional user provided props to override the default props for the OpenSearch Service.| |openSearchDomainName|`string`|Domain name for the OpenSearch Service.| -|cognitoDomainName?|`string`|Optional Cognito domain name, if provided it will be used for Cognito domain, and `openSearchDomainName` will be used for the OpenSearch Service domain.| +|cognitoDomainName?|`string`|Optional Amazon Cognito domain name. If omitted the Amazon Cognito domain will default to the OpenSearch Service domain name.| |createCloudWatchAlarms?|`boolean`|Whether to create the recommended CloudWatch alarms.| |domainEndpointEnvironmentVariableName?|`string`|Optional name for the OpenSearch domain endpoint environment variable set for the Lambda function.| |existingVpc?|[`ec2.IVpc`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ec2.IVpc.html)|An optional, existing VPC into which this pattern should be deployed. When deployed in a VPC, the Lambda function will use ENIs in the VPC to access network resources. If an existing VPC is provided, the `deployVpc` property cannot be `true`. This uses `ec2.IVpc` to allow clients to supply VPCs that exist outside the stack using the [`ec2.Vpc.fromLookup()`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ec2.Vpc.html#static-fromwbrlookupscope-id-options) method.| From 1d012938b91c2e43d37097717bf7c178e665c910 Mon Sep 17 00:00:00 2001 From: mickychetta Date: Tue, 4 Oct 2022 16:59:59 +0000 Subject: [PATCH 6/9] created new construct --- .../aws-lambda-opensearch/.eslintignore | 5 + .../aws-lambda-opensearch/.gitignore | 16 + .../aws-lambda-opensearch/.npmignore | 21 + .../aws-lambda-opensearch/README.md | 8 +- .../aws-lambda-opensearch/lib/index.ts | 174 ++ .../aws-lambda-opensearch/package.json | 103 ++ .../test/integ.cluster-config.expected.json | 982 +++++++++++ .../test/integ.cluster-config.ts | 53 + ...nteg.disabled-zone-awareness.expected.json | 922 +++++++++++ .../test/integ.disabled-zone-awareness.ts | 50 + .../test/integ.domain-arguments.expected.json | 675 ++++++++ .../test/integ.domain-arguments.ts | 40 + .../test/integ.existing-vpc.expected.json | 1431 +++++++++++++++++ .../test/integ.existing-vpc.ts | 44 + .../test/integ.no-arguments.expected.json | 675 ++++++++ .../test/integ.no-arguments.ts | 38 + .../test/integ.vpc-props.expected.json | 1037 ++++++++++++ .../test/integ.vpc-props.ts | 44 + .../test/lambda-opensearch.test.ts | 583 +++++++ .../test/lambda/index.js | 60 + .../@aws-solutions-constructs/core/index.ts | 4 +- .../core/lib/cognito-helper.ts | 15 + .../core/lib/opensearch-defaults.ts | 65 + .../core/lib/opensearch-helper.ts | 306 ++++ .../core/test/opensearch-helper.test.ts | 432 +++++ 25 files changed, 7778 insertions(+), 5 deletions(-) create mode 100644 source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/.eslintignore create mode 100644 source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/.gitignore create mode 100644 source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/.npmignore mode change 100755 => 100644 source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/README.md create mode 100644 source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/lib/index.ts create mode 100644 source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/package.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/test/integ.cluster-config.expected.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/test/integ.cluster-config.ts create mode 100644 source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/test/integ.disabled-zone-awareness.expected.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/test/integ.disabled-zone-awareness.ts create mode 100644 source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/test/integ.domain-arguments.expected.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/test/integ.domain-arguments.ts create mode 100644 source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/test/integ.existing-vpc.expected.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/test/integ.existing-vpc.ts create mode 100644 source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/test/integ.no-arguments.expected.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/test/integ.no-arguments.ts create mode 100644 source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/test/integ.vpc-props.expected.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/test/integ.vpc-props.ts create mode 100644 source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/test/lambda-opensearch.test.ts create mode 100644 source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/test/lambda/index.js create mode 100644 source/patterns/@aws-solutions-constructs/core/lib/opensearch-defaults.ts create mode 100644 source/patterns/@aws-solutions-constructs/core/lib/opensearch-helper.ts create mode 100644 source/patterns/@aws-solutions-constructs/core/test/opensearch-helper.test.ts diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/.eslintignore b/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/.eslintignore new file mode 100644 index 000000000..0819e2e65 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/.eslintignore @@ -0,0 +1,5 @@ +lib/*.js +test/*.js +*.d.ts +coverage +test/lambda/index.js \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/.gitignore b/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/.gitignore new file mode 100644 index 000000000..8626f2274 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/.gitignore @@ -0,0 +1,16 @@ +lib/*.js +test/*.js +!test/lambda/* +*.js.map +*.d.ts +node_modules +*.generated.ts +dist +.jsii + +.LAST_BUILD +.nyc_output +coverage +.nycrc +.LAST_PACKAGE +*.snk \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/.npmignore b/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/.npmignore new file mode 100644 index 000000000..f66791629 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/.npmignore @@ -0,0 +1,21 @@ +# Exclude typescript source and config +*.ts +tsconfig.json +coverage +.nyc_output +*.tgz +*.snk +*.tsbuildinfo + +# Include javascript files and typescript declarations +!*.js +!*.d.ts + +# Exclude jsii outdir +dist + +# Include .jsii +!.jsii + +# Include .jsii +!.jsii \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/README.md b/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/README.md old mode 100755 new mode 100644 index c3d47f10d..350bf3b32 --- a/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/README.md @@ -118,9 +118,9 @@ new LambdaToOpenSearch(this, "sample", |userPool|[`cognito.UserPool`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_cognito.UserPool.html)|Returns an instance of `cognito.UserPool` created by the construct| |userPoolClient|[`cognito.UserPoolClient`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_cognito.UserPoolClient.html)|Returns an instance of `cognito.UserPoolClient` created by the construct| |identityPool|[`cognito.CfnIdentityPool`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_cognito.CfnIdentityPool.html)|Returns an instance of `cognito.CfnIdentityPool` created by the construct| -|opensearchDomain|[`opensearchservice.CfnDomain`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_opensearchservice.CfnDomain.html)|Returns an instance of `opensearch.CfnDomain` created by the construct| -|opensearchRole|[`iam.Role`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_iam.Role.html)|Returns an instance of `iam.Role` created by the construct for `opensearch.CfnDomain`| -|cloudwatchAlarms?|[`cloudwatch.Alarm[]`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_cloudwatch.Alarm.html)|Returns a list of `cloudwatch.Alarm` created by the construct| +|openSearchDomain|[`opensearchservice.CfnDomain`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_opensearchservice.CfnDomain.html)|Returns an instance of `opensearch.CfnDomain` created by the construct| +|openSearchRole|[`iam.Role`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_iam.Role.html)|Returns an instance of `iam.Role` created by the construct for `opensearch.CfnDomain`| +|cloudWatchAlarms?|[`cloudwatch.Alarm[]`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_cloudwatch.Alarm.html)|Returns a list of `cloudwatch.Alarm` created by the construct| |vpc?|[`ec2.IVpc`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ec2.IVpc.html)|Returns an interface on the VPC used by the pattern (if any). This may be a VPC created by the pattern or the VPC supplied to the pattern constructor.| ## Lambda Function @@ -145,7 +145,7 @@ Out of the box implementation of the Construct without any overrides will set th ### Amazon OpenSearch Service * Deploy best practices CloudWatch Alarms for the OpenSearch Service domain -* Secure the Kibana dashboard access with Cognito User Pools +* Secure the OpenSearch Service dashboard access with Cognito User Pools * Enable server-side encryption for OpenSearch Service domain using AWS managed KMS Key * Enable node-to-node encryption for the OpenSearch Service domain * Configure the cluster for the OpenSearch Service domain diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/lib/index.ts b/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/lib/index.ts new file mode 100644 index 000000000..de61df4a6 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/lib/index.ts @@ -0,0 +1,174 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as opensearch from 'aws-cdk-lib/aws-opensearchservice'; +import * as lambda from 'aws-cdk-lib/aws-lambda'; +import * as iam from 'aws-cdk-lib/aws-iam'; +import * as cognito from 'aws-cdk-lib/aws-cognito'; +import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch'; +import * as ec2 from 'aws-cdk-lib/aws-ec2'; +import * as defaults from '@aws-solutions-constructs/core'; +// Note: To ensure CDKv2 compatibility, keep the import statement for Construct separate +import { Construct } from 'constructs'; + +/** + * @summary The properties for the CognitoToApiGatewayToLambda Construct + */ +export interface LambdaToOpenSearchProps { + /** + * Existing instance of Lambda Function object, providing both this and `lambdaFunctionProps` will cause an error. + * + * @default - None + */ + readonly existingLambdaObj?: lambda.Function; + /** + * User provided props to override the default props for the Lambda function. + * + * @default - Default props are used + */ + readonly lambdaFunctionProps?: lambda.FunctionProps; + /** + * Optional user provided props to override the default props for the OpenSearch Service. + * + * @default - Default props are used + */ + readonly openSearchDomainProps?: opensearch.CfnDomainProps; + /** + * Domain name for the OpenSearch Service. + * + * @default - None + */ + readonly openSearchDomainName: string; + /** + * Optional Amazon Cognito domain name. If omitted the Amazon Cognito domain will default to the OpenSearch Service domain name. + * + * @default - None + */ + readonly cognitoDomainName?: string; + /** + * Whether to create recommended CloudWatch alarms + * + * @default - Alarms are created + */ + readonly createCloudWatchAlarms?: boolean; + /** + * Optional Name for the Lambda function environment variable set to the domain endpoint. + * + * @default - DOMAIN_ENDPOINT + */ + readonly domainEndpointEnvironmentVariableName?: string; + /** + * An existing VPC for the construct to use (construct will NOT create a new VPC in this case) + * + * @default - None + */ + readonly existingVpc?: ec2.IVpc; + /** + * Properties to override default properties if deployVpc is true + * + * @default - DefaultIsolatedVpcProps() in vpc-defaults.ts + */ + readonly vpcProps?: ec2.VpcProps; + /** + * Whether to deploy a new VPC + * + * @default - false + */ + readonly deployVpc?: boolean; +} + +export class LambdaToOpenSearch extends Construct { + public readonly lambdaFunction: lambda.Function; + public readonly userPool: cognito.UserPool; + public readonly userPoolClient: cognito.UserPoolClient; + public readonly identityPool: cognito.CfnIdentityPool; + public readonly openSearchDomain: opensearch.CfnDomain; + public readonly openSearchRole: iam.Role; + public readonly cloudWatchAlarms?: cloudwatch.Alarm[]; + public readonly vpc?: ec2.IVpc; + + /** + * @summary Constructs a new instance of the LambdaToOpenSearch class. + * @param {cdk.App} scope - represents the scope for all the resources. + * @param {string} id - this is a a scope-unique id. + * @param {LambdaToOpenSearchProps} props - user provided props for the construct + * @since 0.8.0 + * @access public + */ + constructor(scope: Construct, id: string, props: LambdaToOpenSearchProps) { + super(scope, id); + defaults.CheckProps(props); + + if (props.vpcProps && !props.deployVpc) { + throw new Error("Error - deployVpc must be true when defining vpcProps"); + } + + if (props.lambdaFunctionProps?.vpc || props.lambdaFunctionProps?.vpcSubnets) { + throw new Error("Error - Define VPC using construct parameters not Lambda function props"); + } + + if (props.openSearchDomainProps?.vpcOptions) { + throw new Error("Error - Define VPC using construct parameters not the OpenSearch Service props"); + } + + if (props.deployVpc || props.existingVpc) { + this.vpc = defaults.buildVpc(scope, { + defaultVpcProps: defaults.DefaultIsolatedVpcProps(), + existingVpc: props.existingVpc, + userVpcProps: props.vpcProps, + constructVpcProps: { + enableDnsHostnames: true, + enableDnsSupport: true, + }, + }); + } + + this.lambdaFunction = defaults.buildLambdaFunction(this, { + existingLambdaObj: props.existingLambdaObj, + lambdaFunctionProps: props.lambdaFunctionProps, + vpc: this.vpc + }); + + // Find the lambda service Role ARN + const lambdaFunctionRoleARN = this.lambdaFunction.role?.roleArn; + + let cognitoAuthorizedRole: iam.Role; + + [this.userPool, this.userPoolClient, this.identityPool, cognitoAuthorizedRole] = + defaults.setupOpenSearchCognito(this, props.cognitoDomainName ?? props.openSearchDomainName); + + const buildOpenSearchProps: any = { + userpool: this.userPool, + identitypool: this.identityPool, + cognitoAuthorizedRoleARN: cognitoAuthorizedRole.roleArn, + serviceRoleARN: lambdaFunctionRoleARN, + vpc: this.vpc, + openSearchDomainName: props.openSearchDomainName, + clientDomainProps: props.openSearchDomainProps + }; + + if (this.vpc) { + const securityGroupIds = defaults.getLambdaVpcSecurityGroupIds(this.lambdaFunction); + buildOpenSearchProps.securityGroupIds = securityGroupIds; + } + + [this.openSearchDomain, this.openSearchRole] = defaults.buildOpenSearch(this, buildOpenSearchProps); + + if (props.createCloudWatchAlarms === undefined || props.createCloudWatchAlarms) { + this.cloudWatchAlarms = defaults.buildOpenSearchCWAlarms(this); + } + + const domainEndpointEnvironmentVariableName = props.domainEndpointEnvironmentVariableName || 'DOMAIN_ENDPOINT'; + this.lambdaFunction.addEnvironment(domainEndpointEnvironmentVariableName, this.openSearchDomain.attrDomainEndpoint); + } +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/package.json b/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/package.json new file mode 100644 index 000000000..a2e9b909f --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/package.json @@ -0,0 +1,103 @@ +{ + "name": "@aws-solutions-constructs/aws-lambda-opensearch", + "version": "0.0.0", + "description": "CDK Constructs for AWS Lambda to Amazon OpenSearch Service", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "repository": { + "type": "git", + "url": "https://github.com/awslabs/aws-solutions-constructs.git", + "directory": "source/patterns/@aws-solutions-constructs/aws-lambda-opensearch" + }, + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com", + "organization": true + }, + "license": "Apache-2.0", + "scripts": { + "build": "tsc -b .", + "lint": "eslint -c ../eslintrc.yml --ext=.js,.ts . && tslint --project .", + "lint-fix": "eslint -c ../eslintrc.yml --ext=.js,.ts --fix .", + "test": "jest --coverage", + "clean": "tsc -b --clean", + "watch": "tsc -b -w", + "integ": "cdk-integ", + "integ-no-clean": "cdk-integ --no-clean", + "integ-assert": "cdk-integ-assert", + "jsii": "jsii", + "jsii-pacmak": "jsii-pacmak", + "build+lint+test": "npm run jsii && npm run lint && npm test && npm run integ-assert", + "snapshot-update": "npm run jsii && npm test -- -u && npm run integ-assert" + }, + "jsii": { + "outdir": "dist", + "targets": { + "java": { + "package": "software.amazon.awsconstructs.services.lambdaopensearch", + "maven": { + "groupId": "software.amazon.awsconstructs", + "artifactId": "lambdaopensearch" + } + }, + "dotnet": { + "namespace": "Amazon.SolutionsConstructs.AWS.LambdaOpenSearch", + "packageId": "Amazon.SolutionsConstructs.AWS.LambdaOpenSearch", + "signAssembly": true, + "iconUrl": "https://raw.githubusercontent.com/aws/aws-cdk/master/logo/default-256-dark.png" + }, + "python": { + "distName": "aws-solutions-constructs.aws-lambda-opensearch", + "module": "aws_solutions_constructs.aws_lambda_opensearch" + } + } + }, + "dependencies": { + "@aws-cdk/aws-lambda": "0.0.0", + "@aws-cdk/core": "0.0.0", + "@aws-cdk/aws-cognito": "0.0.0", + "@aws-cdk/aws-ec2": "0.0.0", + "@aws-cdk/aws-opensearchservice": "0.0.0", + "@aws-cdk/aws-iam": "0.0.0", + "@aws-cdk/aws-cloudwatch": "0.0.0", + "@aws-solutions-constructs/core": "0.0.0", + "constructs": "^3.2.0" + }, + "devDependencies": { + "@aws-cdk/assert": "0.0.0", + "@types/jest": "^27.4.0", + "@types/node": "^10.3.0" + }, + "jest": { + "moduleFileExtensions": [ + "js" + ], + "coverageReporters": [ + "text", + [ + "lcov", + { + "projectRoot": "../../../../" + } + ] + ] + }, + "peerDependencies": { + "@aws-cdk/aws-lambda": "0.0.0", + "@aws-cdk/core": "0.0.0", + "@aws-cdk/aws-cognito": "0.0.0", + "@aws-cdk/aws-opensearchservice": "0.0.0", + "@aws-solutions-constructs/core": "0.0.0", + "@aws-cdk/aws-iam": "0.0.0", + "@aws-cdk/aws-cloudwatch": "0.0.0", + "constructs": "^3.2.0" + }, + "keywords": [ + "aws", + "cdk", + "awscdk", + "AWS Solutions Constructs", + "Amazon OpenSearch Service", + "AWS Lambda" + ] + } \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/test/integ.cluster-config.expected.json b/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/test/integ.cluster-config.expected.json new file mode 100644 index 000000000..a5105b28f --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/test/integ.cluster-config.expected.json @@ -0,0 +1,982 @@ +{ + "Resources": { + "testlambdaopensearchLambdaFunctionServiceRole4722AB8A": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:/aws/lambda/*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "LambdaFunctionServiceRolePolicy" + } + ] + } + }, + "testlambdaopensearchLambdaFunctionServiceRoleDefaultPolicy78C56359": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "ec2:CreateNetworkInterface", + "ec2:DescribeNetworkInterfaces", + "ec2:DeleteNetworkInterface", + "ec2:AssignPrivateIpAddresses", + "ec2:UnassignPrivateIpAddresses" + ], + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "xray:PutTraceSegments", + "xray:PutTelemetryRecords" + ], + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "testlambdaopensearchLambdaFunctionServiceRoleDefaultPolicy78C56359", + "Roles": [ + { + "Ref": "testlambdaopensearchLambdaFunctionServiceRole4722AB8A" + } + ] + }, + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W12", + "reason": "Lambda needs the following minimum required permissions to send trace data to X-Ray and access ENIs in a VPC." + } + ] + } + } + }, + "testlambdaopensearchReplaceDefaultSecurityGroupsecuritygroupB44718EC": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "cluster-config/test-lambda-opensearch/ReplaceDefaultSecurityGroup-security-group", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "VpcId": { + "Ref": "Vpc8378EB38" + } + }, + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W5", + "reason": "Egress of 0.0.0.0/0 is default and generally considered OK" + }, + { + "id": "W40", + "reason": "Egress IPProtocol of -1 is default and generally considered OK" + } + ] + } + } + }, + "testlambdaopensearchLambdaFunction93FD38F7": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" + }, + "S3Key": "466cbfc6653782bd2707dec5bc5c58005c75a8bff3660a58eefa57d667120240.zip" + }, + "Role": { + "Fn::GetAtt": [ + "testlambdaopensearchLambdaFunctionServiceRole4722AB8A", + "Arn" + ] + }, + "Environment": { + "Variables": { + "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1", + "DOMAIN_ENDPOINT": { + "Fn::GetAtt": [ + "testlambdaopensearchOpenSearchDomainF9CCC3D3", + "DomainEndpoint" + ] + } + } + }, + "Handler": "index.handler", + "Runtime": "nodejs14.x", + "TracingConfig": { + "Mode": "Active" + }, + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::GetAtt": [ + "testlambdaopensearchReplaceDefaultSecurityGroupsecuritygroupB44718EC", + "GroupId" + ] + } + ], + "SubnetIds": [ + { + "Ref": "VpcisolatedSubnet1SubnetE62B1B9B" + }, + { + "Ref": "VpcisolatedSubnet2Subnet39217055" + } + ] + } + }, + "DependsOn": [ + "testlambdaopensearchLambdaFunctionServiceRoleDefaultPolicy78C56359", + "testlambdaopensearchLambdaFunctionServiceRole4722AB8A", + "VpcisolatedSubnet1RouteTableAssociationD259E31A", + "VpcisolatedSubnet2RouteTableAssociation25A4716F" + ], + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W58", + "reason": "Lambda functions has the required permission to write CloudWatch Logs. It uses custom policy instead of arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole with tighter permissions." + }, + { + "id": "W89", + "reason": "This is not a rule for the general case, just for specific use cases/industries" + }, + { + "id": "W92", + "reason": "Impossible for us to define the correct concurrency for clients" + } + ] + } + } + }, + "testlambdaopensearchCognitoUserPoolA09096F9": { + "Type": "AWS::Cognito::UserPool", + "Properties": { + "AccountRecoverySetting": { + "RecoveryMechanisms": [ + { + "Name": "verified_phone_number", + "Priority": 1 + }, + { + "Name": "verified_email", + "Priority": 2 + } + ] + }, + "AdminCreateUserConfig": { + "AllowAdminCreateUserOnly": true + }, + "EmailVerificationMessage": "The verification code to your new account is {####}", + "EmailVerificationSubject": "Verify your new account", + "SmsVerificationMessage": "The verification code to your new account is {####}", + "UserPoolAddOns": { + "AdvancedSecurityMode": "ENFORCED" + }, + "VerificationMessageTemplate": { + "DefaultEmailOption": "CONFIRM_WITH_CODE", + "EmailMessage": "The verification code to your new account is {####}", + "EmailSubject": "Verify your new account", + "SmsMessage": "The verification code to your new account is {####}" + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "testlambdaopensearchCognitoUserPoolClient39C21D94": { + "Type": "AWS::Cognito::UserPoolClient", + "Properties": { + "UserPoolId": { + "Ref": "testlambdaopensearchCognitoUserPoolA09096F9" + }, + "AllowedOAuthFlows": [ + "implicit", + "code" + ], + "AllowedOAuthFlowsUserPoolClient": true, + "AllowedOAuthScopes": [ + "profile", + "phone", + "email", + "openid", + "aws.cognito.signin.user.admin" + ], + "CallbackURLs": [ + "https://example.com" + ], + "SupportedIdentityProviders": [ + "COGNITO" + ] + } + }, + "testlambdaopensearchCognitoIdentityPool0B1FB311": { + "Type": "AWS::Cognito::IdentityPool", + "Properties": { + "AllowUnauthenticatedIdentities": false, + "CognitoIdentityProviders": [ + { + "ClientId": { + "Ref": "testlambdaopensearchCognitoUserPoolClient39C21D94" + }, + "ProviderName": { + "Fn::GetAtt": [ + "testlambdaopensearchCognitoUserPoolA09096F9", + "ProviderName" + ] + }, + "ServerSideTokenCheck": true + } + ] + } + }, + "testlambdaopensearchUserPoolDomain98864920": { + "Type": "AWS::Cognito::UserPoolDomain", + "Properties": { + "Domain": "deploytestwithclusterconfig", + "UserPoolId": { + "Ref": "testlambdaopensearchCognitoUserPoolA09096F9" + } + }, + "DependsOn": [ + "testlambdaopensearchCognitoUserPoolA09096F9" + ] + }, + "testlambdaopensearchCognitoAuthorizedRole58A1ED44": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRoleWithWebIdentity", + "Condition": { + "StringEquals": { + "cognito-identity.amazonaws.com:aud": { + "Ref": "testlambdaopensearchCognitoIdentityPool0B1FB311" + } + }, + "ForAnyValue:StringLike": { + "cognito-identity.amazonaws.com:amr": "authenticated" + } + }, + "Effect": "Allow", + "Principal": { + "Federated": "cognito-identity.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": "es:ESHttp*", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":es:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":domain/deploytestwithclusterconfig/*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "CognitoAccessPolicy" + } + ] + } + }, + "testlambdaopensearchIdentityPoolRoleMappingD8C765B1": { + "Type": "AWS::Cognito::IdentityPoolRoleAttachment", + "Properties": { + "IdentityPoolId": { + "Ref": "testlambdaopensearchCognitoIdentityPool0B1FB311" + }, + "Roles": { + "authenticated": { + "Fn::GetAtt": [ + "testlambdaopensearchCognitoAuthorizedRole58A1ED44", + "Arn" + ] + } + } + } + }, + "testlambdaopensearchCognitoKibanaConfigureRole9623271F": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "es.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "testlambdaopensearchCognitoKibanaConfigureRolePolicyE9D9C211": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "cognito-idp:DescribeUserPool", + "cognito-idp:CreateUserPoolClient", + "cognito-idp:DeleteUserPoolClient", + "cognito-idp:DescribeUserPoolClient", + "cognito-idp:AdminInitiateAuth", + "cognito-idp:AdminUserGlobalSignOut", + "cognito-idp:ListUserPoolClients", + "cognito-identity:DescribeIdentityPool", + "cognito-identity:UpdateIdentityPool", + "cognito-identity:SetIdentityPoolRoles", + "cognito-identity:GetIdentityPoolRoles", + "es:UpdateDomainConfig" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "testlambdaopensearchCognitoUserPoolA09096F9", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:aws:cognito-identity:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":identitypool/", + { + "Ref": "testlambdaopensearchCognitoIdentityPool0B1FB311" + } + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:aws:es:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":domain/deploytestwithclusterconfig" + ] + ] + } + ] + }, + { + "Action": "iam:PassRole", + "Condition": { + "StringLike": { + "iam:PassedToService": "cognito-identity.amazonaws.com" + } + }, + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "testlambdaopensearchCognitoKibanaConfigureRole9623271F", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "testlambdaopensearchCognitoKibanaConfigureRolePolicyE9D9C211", + "Roles": [ + { + "Ref": "testlambdaopensearchCognitoKibanaConfigureRole9623271F" + } + ] + } + }, + "testlambdaopensearchOpenSearchDomainF9CCC3D3": { + "Type": "AWS::OpenSearchService::Domain", + "Properties": { + "AccessPolicies": { + "Statement": [ + { + "Action": "es:ESHttp*", + "Effect": "Allow", + "Principal": { + "AWS": [ + { + "Fn::GetAtt": [ + "testlambdaopensearchCognitoAuthorizedRole58A1ED44", + "Arn" + ] + }, + { + "Fn::GetAtt": [ + "testlambdaopensearchLambdaFunctionServiceRole4722AB8A", + "Arn" + ] + } + ] + }, + "Resource": { + "Fn::Join": [ + "", + [ + "arn:aws:es:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":domain/deploytestwithclusterconfig/*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "ClusterConfig": { + "DedicatedMasterCount": 3, + "DedicatedMasterEnabled": true, + "InstanceCount": 2, + "ZoneAwarenessConfig": { + "AvailabilityZoneCount": 2 + }, + "ZoneAwarenessEnabled": true + }, + "CognitoOptions": { + "Enabled": true, + "IdentityPoolId": { + "Ref": "testlambdaopensearchCognitoIdentityPool0B1FB311" + }, + "RoleArn": { + "Fn::GetAtt": [ + "testlambdaopensearchCognitoKibanaConfigureRole9623271F", + "Arn" + ] + }, + "UserPoolId": { + "Ref": "testlambdaopensearchCognitoUserPoolA09096F9" + } + }, + "DomainName": "deploytestwithclusterconfig", + "EBSOptions": { + "EBSEnabled": true, + "VolumeSize": 10 + }, + "EncryptionAtRestOptions": { + "Enabled": true + }, + "EngineVersion": "OpenSearch_1.3", + "NodeToNodeEncryptionOptions": { + "Enabled": true + }, + "SnapshotOptions": { + "AutomatedSnapshotStartHour": 1 + }, + "VPCOptions": { + "SecurityGroupIds": [ + { + "Fn::GetAtt": [ + "testlambdaopensearchReplaceDefaultSecurityGroupsecuritygroupB44718EC", + "GroupId" + ] + } + ], + "SubnetIds": [ + { + "Ref": "VpcisolatedSubnet1SubnetE62B1B9B" + }, + { + "Ref": "VpcisolatedSubnet2Subnet39217055" + } + ] + } + }, + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W28", + "reason": "The OpenSearch Service domain is passed dynamically as as parameter and explicitly specified to ensure that IAM policies are configured to lockdown access to this specific OpenSearch Service instance only" + }, + { + "id": "W90", + "reason": "This is not a rule for the general case, just for specific use cases/industries" + } + ] + } + } + }, + "testlambdaopensearchStatusRedAlarm1627144D": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "AlarmDescription": "At least one primary shard and its replicas are not allocated to a node. ", + "MetricName": "ClusterStatus.red", + "Namespace": "AWS/ES", + "Period": 60, + "Statistic": "Maximum", + "Threshold": 1 + } + }, + "testlambdaopensearchStatusYellowAlarm57139CF0": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "AlarmDescription": "At least one replica shard is not allocated to a node.", + "MetricName": "ClusterStatus.yellow", + "Namespace": "AWS/ES", + "Period": 60, + "Statistic": "Maximum", + "Threshold": 1 + } + }, + "testlambdaopensearchFreeStorageSpaceTooLowAlarm6A5E1E96": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "LessThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "AlarmDescription": "A node in your cluster is down to 20 GiB of free storage space.", + "MetricName": "FreeStorageSpace", + "Namespace": "AWS/ES", + "Period": 60, + "Statistic": "Minimum", + "Threshold": 20000 + } + }, + "testlambdaopensearchIndexWritesBlockedTooHighAlarmD2E041A3": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "AlarmDescription": "Your cluster is blocking write requests.", + "MetricName": "ClusterIndexWritesBlocked", + "Namespace": "AWS/ES", + "Period": 300, + "Statistic": "Maximum", + "Threshold": 1 + } + }, + "testlambdaopensearchAutomatedSnapshotFailureTooHighAlarm9A4D0B1F": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "AlarmDescription": "An automated snapshot failed. This failure is often the result of a red cluster health status.", + "MetricName": "AutomatedSnapshotFailure", + "Namespace": "AWS/ES", + "Period": 60, + "Statistic": "Maximum", + "Threshold": 1 + } + }, + "testlambdaopensearchCPUUtilizationTooHighAlarmC4850758": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 3, + "AlarmDescription": "100% CPU utilization is not uncommon, but sustained high usage is problematic. Consider using larger instance types or adding instances.", + "MetricName": "CPUUtilization", + "Namespace": "AWS/ES", + "Period": 900, + "Statistic": "Average", + "Threshold": 80 + } + }, + "testlambdaopensearchJVMMemoryPressureTooHighAlarmEFB09A7C": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "AlarmDescription": "Average JVM memory pressure over last 15 minutes too high. Consider scaling vertically.", + "MetricName": "JVMMemoryPressure", + "Namespace": "AWS/ES", + "Period": 900, + "Statistic": "Average", + "Threshold": 80 + } + }, + "testlambdaopensearchMasterCPUUtilizationTooHighAlarm124D5748": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 3, + "AlarmDescription": "Average CPU utilization over last 45 minutes too high. Consider using larger instance types for your dedicated master nodes.", + "MetricName": "MasterCPUUtilization", + "Namespace": "AWS/ES", + "Period": 900, + "Statistic": "Average", + "Threshold": 50 + } + }, + "testlambdaopensearchMasterJVMMemoryPressureTooHighAlarmBC9524D3": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "AlarmDescription": "Average JVM memory pressure over last 15 minutes too high. Consider scaling vertically.", + "MetricName": "MasterJVMMemoryPressure", + "Namespace": "AWS/ES", + "Period": 900, + "Statistic": "Average", + "Threshold": 50 + } + }, + "Vpc8378EB38": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "10.0.0.0/16", + "EnableDnsHostnames": true, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": [ + { + "Key": "Name", + "Value": "cluster-config/Vpc" + } + ] + } + }, + "VpcisolatedSubnet1SubnetE62B1B9B": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1a", + "CidrBlock": "10.0.0.0/18", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "isolated" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Isolated" + }, + { + "Key": "Name", + "Value": "cluster-config/Vpc/isolatedSubnet1" + } + ] + } + }, + "VpcisolatedSubnet1RouteTableE442650B": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "cluster-config/Vpc/isolatedSubnet1" + } + ] + } + }, + "VpcisolatedSubnet1RouteTableAssociationD259E31A": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcisolatedSubnet1RouteTableE442650B" + }, + "SubnetId": { + "Ref": "VpcisolatedSubnet1SubnetE62B1B9B" + } + } + }, + "VpcisolatedSubnet2Subnet39217055": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1b", + "CidrBlock": "10.0.64.0/18", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "isolated" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Isolated" + }, + { + "Key": "Name", + "Value": "cluster-config/Vpc/isolatedSubnet2" + } + ] + } + }, + "VpcisolatedSubnet2RouteTable334F9764": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "cluster-config/Vpc/isolatedSubnet2" + } + ] + } + }, + "VpcisolatedSubnet2RouteTableAssociation25A4716F": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcisolatedSubnet2RouteTable334F9764" + }, + "SubnetId": { + "Ref": "VpcisolatedSubnet2Subnet39217055" + } + } + }, + "VpcFlowLogIAMRole6A475D41": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "vpc-flow-logs.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Tags": [ + { + "Key": "Name", + "Value": "cluster-config/Vpc" + } + ] + } + }, + "VpcFlowLogIAMRoleDefaultPolicy406FB995": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogStream", + "logs:PutLogEvents", + "logs:DescribeLogStreams" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "VpcFlowLogLogGroup7B5C56B9", + "Arn" + ] + } + }, + { + "Action": "iam:PassRole", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "VpcFlowLogIAMRole6A475D41", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "VpcFlowLogIAMRoleDefaultPolicy406FB995", + "Roles": [ + { + "Ref": "VpcFlowLogIAMRole6A475D41" + } + ] + } + }, + "VpcFlowLogLogGroup7B5C56B9": { + "Type": "AWS::Logs::LogGroup", + "Properties": { + "RetentionInDays": 731, + "Tags": [ + { + "Key": "Name", + "Value": "cluster-config/Vpc" + } + ] + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain", + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W84", + "reason": "By default CloudWatchLogs LogGroups data is encrypted using the CloudWatch server-side encryption keys (AWS Managed Keys)" + } + ] + } + } + }, + "VpcFlowLog8FF33A73": { + "Type": "AWS::EC2::FlowLog", + "Properties": { + "ResourceId": { + "Ref": "Vpc8378EB38" + }, + "ResourceType": "VPC", + "TrafficType": "ALL", + "DeliverLogsPermissionArn": { + "Fn::GetAtt": [ + "VpcFlowLogIAMRole6A475D41", + "Arn" + ] + }, + "LogDestinationType": "cloud-watch-logs", + "LogGroupName": { + "Ref": "VpcFlowLogLogGroup7B5C56B9" + }, + "Tags": [ + { + "Key": "Name", + "Value": "cluster-config/Vpc" + } + ] + } + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/test/integ.cluster-config.ts b/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/test/integ.cluster-config.ts new file mode 100644 index 000000000..bc271b4b7 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/test/integ.cluster-config.ts @@ -0,0 +1,53 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +/// !cdk-integ * +import { App, Stack } from "aws-cdk-lib"; +import { LambdaToOpenSearch } from "../lib"; +import * as lambda from 'aws-cdk-lib/aws-lambda'; +import * as defaults from '@aws-solutions-constructs/core'; + +// Setup +const app = new App(); +const stack = new Stack(app, defaults.generateIntegStackName(__filename), {}); + +const lambdaProps: lambda.FunctionProps = { + code: lambda.Code.fromAsset(`${__dirname}/lambda`), + runtime: lambda.Runtime.NODEJS_14_X, + handler: 'index.handler', +}; + +const openSearchDomainProps = { + clusterConfig: { + dedicatedMasterEnabled: true, + dedicatedMasterCount: 3, + instanceCount: 2, + zoneAwarenessEnabled: true, + zoneAwarenessConfig: { + availabilityZoneCount: 2 + } + } +}; + +new LambdaToOpenSearch(stack, 'test-lambda-opensearch', { + lambdaFunctionProps: lambdaProps, + openSearchDomainName: "deploytestwithclusterconfig", + openSearchDomainProps, + deployVpc: true, + vpcProps: { + maxAzs: 2 + } +}); + +// Synth +app.synth(); \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/test/integ.disabled-zone-awareness.expected.json b/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/test/integ.disabled-zone-awareness.expected.json new file mode 100644 index 000000000..51d046c24 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/test/integ.disabled-zone-awareness.expected.json @@ -0,0 +1,922 @@ +{ + "Resources": { + "testlambdaopensearchLambdaFunctionServiceRole4722AB8A": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:/aws/lambda/*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "LambdaFunctionServiceRolePolicy" + } + ] + } + }, + "testlambdaopensearchLambdaFunctionServiceRoleDefaultPolicy78C56359": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "ec2:CreateNetworkInterface", + "ec2:DescribeNetworkInterfaces", + "ec2:DeleteNetworkInterface", + "ec2:AssignPrivateIpAddresses", + "ec2:UnassignPrivateIpAddresses" + ], + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "xray:PutTraceSegments", + "xray:PutTelemetryRecords" + ], + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "testlambdaopensearchLambdaFunctionServiceRoleDefaultPolicy78C56359", + "Roles": [ + { + "Ref": "testlambdaopensearchLambdaFunctionServiceRole4722AB8A" + } + ] + }, + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W12", + "reason": "Lambda needs the following minimum required permissions to send trace data to X-Ray and access ENIs in a VPC." + } + ] + } + } + }, + "testlambdaopensearchReplaceDefaultSecurityGroupsecuritygroupB44718EC": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "disabled-zone-awareness/test-lambda-opensearch/ReplaceDefaultSecurityGroup-security-group", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "VpcId": { + "Ref": "Vpc8378EB38" + } + }, + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W5", + "reason": "Egress of 0.0.0.0/0 is default and generally considered OK" + }, + { + "id": "W40", + "reason": "Egress IPProtocol of -1 is default and generally considered OK" + } + ] + } + } + }, + "testlambdaopensearchLambdaFunction93FD38F7": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" + }, + "S3Key": "466cbfc6653782bd2707dec5bc5c58005c75a8bff3660a58eefa57d667120240.zip" + }, + "Role": { + "Fn::GetAtt": [ + "testlambdaopensearchLambdaFunctionServiceRole4722AB8A", + "Arn" + ] + }, + "Environment": { + "Variables": { + "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1", + "DOMAIN_ENDPOINT": { + "Fn::GetAtt": [ + "testlambdaopensearchOpenSearchDomainF9CCC3D3", + "DomainEndpoint" + ] + } + } + }, + "Handler": "index.handler", + "Runtime": "nodejs14.x", + "TracingConfig": { + "Mode": "Active" + }, + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::GetAtt": [ + "testlambdaopensearchReplaceDefaultSecurityGroupsecuritygroupB44718EC", + "GroupId" + ] + } + ], + "SubnetIds": [ + { + "Ref": "VpcisolatedSubnet1SubnetE62B1B9B" + } + ] + } + }, + "DependsOn": [ + "testlambdaopensearchLambdaFunctionServiceRoleDefaultPolicy78C56359", + "testlambdaopensearchLambdaFunctionServiceRole4722AB8A", + "VpcisolatedSubnet1RouteTableAssociationD259E31A" + ], + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W58", + "reason": "Lambda functions has the required permission to write CloudWatch Logs. It uses custom policy instead of arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole with tighter permissions." + }, + { + "id": "W89", + "reason": "This is not a rule for the general case, just for specific use cases/industries" + }, + { + "id": "W92", + "reason": "Impossible for us to define the correct concurrency for clients" + } + ] + } + } + }, + "testlambdaopensearchCognitoUserPoolA09096F9": { + "Type": "AWS::Cognito::UserPool", + "Properties": { + "AccountRecoverySetting": { + "RecoveryMechanisms": [ + { + "Name": "verified_phone_number", + "Priority": 1 + }, + { + "Name": "verified_email", + "Priority": 2 + } + ] + }, + "AdminCreateUserConfig": { + "AllowAdminCreateUserOnly": true + }, + "EmailVerificationMessage": "The verification code to your new account is {####}", + "EmailVerificationSubject": "Verify your new account", + "SmsVerificationMessage": "The verification code to your new account is {####}", + "UserPoolAddOns": { + "AdvancedSecurityMode": "ENFORCED" + }, + "VerificationMessageTemplate": { + "DefaultEmailOption": "CONFIRM_WITH_CODE", + "EmailMessage": "The verification code to your new account is {####}", + "EmailSubject": "Verify your new account", + "SmsMessage": "The verification code to your new account is {####}" + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "testlambdaopensearchCognitoUserPoolClient39C21D94": { + "Type": "AWS::Cognito::UserPoolClient", + "Properties": { + "UserPoolId": { + "Ref": "testlambdaopensearchCognitoUserPoolA09096F9" + }, + "AllowedOAuthFlows": [ + "implicit", + "code" + ], + "AllowedOAuthFlowsUserPoolClient": true, + "AllowedOAuthScopes": [ + "profile", + "phone", + "email", + "openid", + "aws.cognito.signin.user.admin" + ], + "CallbackURLs": [ + "https://example.com" + ], + "SupportedIdentityProviders": [ + "COGNITO" + ] + } + }, + "testlambdaopensearchCognitoIdentityPool0B1FB311": { + "Type": "AWS::Cognito::IdentityPool", + "Properties": { + "AllowUnauthenticatedIdentities": false, + "CognitoIdentityProviders": [ + { + "ClientId": { + "Ref": "testlambdaopensearchCognitoUserPoolClient39C21D94" + }, + "ProviderName": { + "Fn::GetAtt": [ + "testlambdaopensearchCognitoUserPoolA09096F9", + "ProviderName" + ] + }, + "ServerSideTokenCheck": true + } + ] + } + }, + "testlambdaopensearchUserPoolDomain98864920": { + "Type": "AWS::Cognito::UserPoolDomain", + "Properties": { + "Domain": "disabledzoneawareness", + "UserPoolId": { + "Ref": "testlambdaopensearchCognitoUserPoolA09096F9" + } + }, + "DependsOn": [ + "testlambdaopensearchCognitoUserPoolA09096F9" + ] + }, + "testlambdaopensearchCognitoAuthorizedRole58A1ED44": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRoleWithWebIdentity", + "Condition": { + "StringEquals": { + "cognito-identity.amazonaws.com:aud": { + "Ref": "testlambdaopensearchCognitoIdentityPool0B1FB311" + } + }, + "ForAnyValue:StringLike": { + "cognito-identity.amazonaws.com:amr": "authenticated" + } + }, + "Effect": "Allow", + "Principal": { + "Federated": "cognito-identity.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": "es:ESHttp*", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":es:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":domain/disabledzoneawareness/*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "CognitoAccessPolicy" + } + ] + } + }, + "testlambdaopensearchIdentityPoolRoleMappingD8C765B1": { + "Type": "AWS::Cognito::IdentityPoolRoleAttachment", + "Properties": { + "IdentityPoolId": { + "Ref": "testlambdaopensearchCognitoIdentityPool0B1FB311" + }, + "Roles": { + "authenticated": { + "Fn::GetAtt": [ + "testlambdaopensearchCognitoAuthorizedRole58A1ED44", + "Arn" + ] + } + } + } + }, + "testlambdaopensearchCognitoKibanaConfigureRole9623271F": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "es.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "testlambdaopensearchCognitoKibanaConfigureRolePolicyE9D9C211": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "cognito-idp:DescribeUserPool", + "cognito-idp:CreateUserPoolClient", + "cognito-idp:DeleteUserPoolClient", + "cognito-idp:DescribeUserPoolClient", + "cognito-idp:AdminInitiateAuth", + "cognito-idp:AdminUserGlobalSignOut", + "cognito-idp:ListUserPoolClients", + "cognito-identity:DescribeIdentityPool", + "cognito-identity:UpdateIdentityPool", + "cognito-identity:SetIdentityPoolRoles", + "cognito-identity:GetIdentityPoolRoles", + "es:UpdateDomainConfig" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "testlambdaopensearchCognitoUserPoolA09096F9", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:aws:cognito-identity:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":identitypool/", + { + "Ref": "testlambdaopensearchCognitoIdentityPool0B1FB311" + } + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:aws:es:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":domain/disabledzoneawareness" + ] + ] + } + ] + }, + { + "Action": "iam:PassRole", + "Condition": { + "StringLike": { + "iam:PassedToService": "cognito-identity.amazonaws.com" + } + }, + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "testlambdaopensearchCognitoKibanaConfigureRole9623271F", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "testlambdaopensearchCognitoKibanaConfigureRolePolicyE9D9C211", + "Roles": [ + { + "Ref": "testlambdaopensearchCognitoKibanaConfigureRole9623271F" + } + ] + } + }, + "testlambdaopensearchOpenSearchDomainF9CCC3D3": { + "Type": "AWS::OpenSearchService::Domain", + "Properties": { + "AccessPolicies": { + "Statement": [ + { + "Action": "es:ESHttp*", + "Effect": "Allow", + "Principal": { + "AWS": [ + { + "Fn::GetAtt": [ + "testlambdaopensearchCognitoAuthorizedRole58A1ED44", + "Arn" + ] + }, + { + "Fn::GetAtt": [ + "testlambdaopensearchLambdaFunctionServiceRole4722AB8A", + "Arn" + ] + } + ] + }, + "Resource": { + "Fn::Join": [ + "", + [ + "arn:aws:es:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":domain/disabledzoneawareness/*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "ClusterConfig": { + "DedicatedMasterCount": 3, + "DedicatedMasterEnabled": true, + "InstanceCount": 3, + "ZoneAwarenessEnabled": false + }, + "CognitoOptions": { + "Enabled": true, + "IdentityPoolId": { + "Ref": "testlambdaopensearchCognitoIdentityPool0B1FB311" + }, + "RoleArn": { + "Fn::GetAtt": [ + "testlambdaopensearchCognitoKibanaConfigureRole9623271F", + "Arn" + ] + }, + "UserPoolId": { + "Ref": "testlambdaopensearchCognitoUserPoolA09096F9" + } + }, + "DomainName": "disabledzoneawareness", + "EBSOptions": { + "EBSEnabled": true, + "VolumeSize": 10 + }, + "EncryptionAtRestOptions": { + "Enabled": true + }, + "EngineVersion": "OpenSearch_1.3", + "NodeToNodeEncryptionOptions": { + "Enabled": true + }, + "SnapshotOptions": { + "AutomatedSnapshotStartHour": 1 + }, + "VPCOptions": { + "SecurityGroupIds": [ + { + "Fn::GetAtt": [ + "testlambdaopensearchReplaceDefaultSecurityGroupsecuritygroupB44718EC", + "GroupId" + ] + } + ], + "SubnetIds": [ + { + "Ref": "VpcisolatedSubnet1SubnetE62B1B9B" + } + ] + } + }, + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W28", + "reason": "The OpenSearch Service domain is passed dynamically as as parameter and explicitly specified to ensure that IAM policies are configured to lockdown access to this specific OpenSearch Service instance only" + }, + { + "id": "W90", + "reason": "This is not a rule for the general case, just for specific use cases/industries" + } + ] + } + } + }, + "testlambdaopensearchStatusRedAlarm1627144D": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "AlarmDescription": "At least one primary shard and its replicas are not allocated to a node. ", + "MetricName": "ClusterStatus.red", + "Namespace": "AWS/ES", + "Period": 60, + "Statistic": "Maximum", + "Threshold": 1 + } + }, + "testlambdaopensearchStatusYellowAlarm57139CF0": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "AlarmDescription": "At least one replica shard is not allocated to a node.", + "MetricName": "ClusterStatus.yellow", + "Namespace": "AWS/ES", + "Period": 60, + "Statistic": "Maximum", + "Threshold": 1 + } + }, + "testlambdaopensearchFreeStorageSpaceTooLowAlarm6A5E1E96": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "LessThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "AlarmDescription": "A node in your cluster is down to 20 GiB of free storage space.", + "MetricName": "FreeStorageSpace", + "Namespace": "AWS/ES", + "Period": 60, + "Statistic": "Minimum", + "Threshold": 20000 + } + }, + "testlambdaopensearchIndexWritesBlockedTooHighAlarmD2E041A3": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "AlarmDescription": "Your cluster is blocking write requests.", + "MetricName": "ClusterIndexWritesBlocked", + "Namespace": "AWS/ES", + "Period": 300, + "Statistic": "Maximum", + "Threshold": 1 + } + }, + "testlambdaopensearchAutomatedSnapshotFailureTooHighAlarm9A4D0B1F": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "AlarmDescription": "An automated snapshot failed. This failure is often the result of a red cluster health status.", + "MetricName": "AutomatedSnapshotFailure", + "Namespace": "AWS/ES", + "Period": 60, + "Statistic": "Maximum", + "Threshold": 1 + } + }, + "testlambdaopensearchCPUUtilizationTooHighAlarmC4850758": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 3, + "AlarmDescription": "100% CPU utilization is not uncommon, but sustained high usage is problematic. Consider using larger instance types or adding instances.", + "MetricName": "CPUUtilization", + "Namespace": "AWS/ES", + "Period": 900, + "Statistic": "Average", + "Threshold": 80 + } + }, + "testlambdaopensearchJVMMemoryPressureTooHighAlarmEFB09A7C": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "AlarmDescription": "Average JVM memory pressure over last 15 minutes too high. Consider scaling vertically.", + "MetricName": "JVMMemoryPressure", + "Namespace": "AWS/ES", + "Period": 900, + "Statistic": "Average", + "Threshold": 80 + } + }, + "testlambdaopensearchMasterCPUUtilizationTooHighAlarm124D5748": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 3, + "AlarmDescription": "Average CPU utilization over last 45 minutes too high. Consider using larger instance types for your dedicated master nodes.", + "MetricName": "MasterCPUUtilization", + "Namespace": "AWS/ES", + "Period": 900, + "Statistic": "Average", + "Threshold": 50 + } + }, + "testlambdaopensearchMasterJVMMemoryPressureTooHighAlarmBC9524D3": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "AlarmDescription": "Average JVM memory pressure over last 15 minutes too high. Consider scaling vertically.", + "MetricName": "MasterJVMMemoryPressure", + "Namespace": "AWS/ES", + "Period": 900, + "Statistic": "Average", + "Threshold": 50 + } + }, + "Vpc8378EB38": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "10.0.0.0/16", + "EnableDnsHostnames": true, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": [ + { + "Key": "Name", + "Value": "disabled-zone-awareness/Vpc" + } + ] + } + }, + "VpcisolatedSubnet1SubnetE62B1B9B": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1a", + "CidrBlock": "10.0.0.0/18", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "isolated" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Isolated" + }, + { + "Key": "Name", + "Value": "disabled-zone-awareness/Vpc/isolatedSubnet1" + } + ] + } + }, + "VpcisolatedSubnet1RouteTableE442650B": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "disabled-zone-awareness/Vpc/isolatedSubnet1" + } + ] + } + }, + "VpcisolatedSubnet1RouteTableAssociationD259E31A": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcisolatedSubnet1RouteTableE442650B" + }, + "SubnetId": { + "Ref": "VpcisolatedSubnet1SubnetE62B1B9B" + } + } + }, + "VpcFlowLogIAMRole6A475D41": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "vpc-flow-logs.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Tags": [ + { + "Key": "Name", + "Value": "disabled-zone-awareness/Vpc" + } + ] + } + }, + "VpcFlowLogIAMRoleDefaultPolicy406FB995": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogStream", + "logs:PutLogEvents", + "logs:DescribeLogStreams" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "VpcFlowLogLogGroup7B5C56B9", + "Arn" + ] + } + }, + { + "Action": "iam:PassRole", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "VpcFlowLogIAMRole6A475D41", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "VpcFlowLogIAMRoleDefaultPolicy406FB995", + "Roles": [ + { + "Ref": "VpcFlowLogIAMRole6A475D41" + } + ] + } + }, + "VpcFlowLogLogGroup7B5C56B9": { + "Type": "AWS::Logs::LogGroup", + "Properties": { + "RetentionInDays": 731, + "Tags": [ + { + "Key": "Name", + "Value": "disabled-zone-awareness/Vpc" + } + ] + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain", + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W84", + "reason": "By default CloudWatchLogs LogGroups data is encrypted using the CloudWatch server-side encryption keys (AWS Managed Keys)" + } + ] + } + } + }, + "VpcFlowLog8FF33A73": { + "Type": "AWS::EC2::FlowLog", + "Properties": { + "ResourceId": { + "Ref": "Vpc8378EB38" + }, + "ResourceType": "VPC", + "TrafficType": "ALL", + "DeliverLogsPermissionArn": { + "Fn::GetAtt": [ + "VpcFlowLogIAMRole6A475D41", + "Arn" + ] + }, + "LogDestinationType": "cloud-watch-logs", + "LogGroupName": { + "Ref": "VpcFlowLogLogGroup7B5C56B9" + }, + "Tags": [ + { + "Key": "Name", + "Value": "disabled-zone-awareness/Vpc" + } + ] + } + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/test/integ.disabled-zone-awareness.ts b/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/test/integ.disabled-zone-awareness.ts new file mode 100644 index 000000000..283874a75 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/test/integ.disabled-zone-awareness.ts @@ -0,0 +1,50 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +/// !cdk-integ * +import { App, Stack } from "aws-cdk-lib"; +import { LambdaToOpenSearch } from "../lib"; +import * as lambda from 'aws-cdk-lib/aws-lambda'; +import * as defaults from '@aws-solutions-constructs/core'; + +// Setup +const app = new App(); +const stack = new Stack(app, defaults.generateIntegStackName(__filename), {}); + +const lambdaProps: lambda.FunctionProps = { + code: lambda.Code.fromAsset(`${__dirname}/lambda`), + runtime: lambda.Runtime.NODEJS_14_X, + handler: 'index.handler', +}; + +const openSearchDomainProps = { + clusterConfig: { + dedicatedMasterCount: 3, + dedicatedMasterEnabled: true, + instanceCount: 3, + zoneAwarenessEnabled: false, + } +}; + +new LambdaToOpenSearch(stack, 'test-lambda-opensearch', { + lambdaFunctionProps: lambdaProps, + openSearchDomainName: "disabledzoneawareness", + openSearchDomainProps, + deployVpc: true, + vpcProps: { + maxAzs: 1 + } +}); + +// Synth +app.synth(); \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/test/integ.domain-arguments.expected.json b/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/test/integ.domain-arguments.expected.json new file mode 100644 index 000000000..3c73408ee --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/test/integ.domain-arguments.expected.json @@ -0,0 +1,675 @@ +{ + "Resources": { + "testlambdaopensearchLambdaFunctionServiceRole4722AB8A": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:/aws/lambda/*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "LambdaFunctionServiceRolePolicy" + } + ] + } + }, + "testlambdaopensearchLambdaFunctionServiceRoleDefaultPolicy78C56359": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "xray:PutTraceSegments", + "xray:PutTelemetryRecords" + ], + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "testlambdaopensearchLambdaFunctionServiceRoleDefaultPolicy78C56359", + "Roles": [ + { + "Ref": "testlambdaopensearchLambdaFunctionServiceRole4722AB8A" + } + ] + }, + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W12", + "reason": "Lambda needs the following minimum required permissions to send trace data to X-Ray and access ENIs in a VPC." + } + ] + } + } + }, + "testlambdaopensearchLambdaFunction93FD38F7": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" + }, + "S3Key": "466cbfc6653782bd2707dec5bc5c58005c75a8bff3660a58eefa57d667120240.zip" + }, + "Role": { + "Fn::GetAtt": [ + "testlambdaopensearchLambdaFunctionServiceRole4722AB8A", + "Arn" + ] + }, + "Environment": { + "Variables": { + "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1", + "DOMAIN_ENDPOINT": { + "Fn::GetAtt": [ + "testlambdaopensearchOpenSearchDomainF9CCC3D3", + "DomainEndpoint" + ] + } + } + }, + "Handler": "index.handler", + "Runtime": "nodejs14.x", + "TracingConfig": { + "Mode": "Active" + } + }, + "DependsOn": [ + "testlambdaopensearchLambdaFunctionServiceRoleDefaultPolicy78C56359", + "testlambdaopensearchLambdaFunctionServiceRole4722AB8A" + ], + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W58", + "reason": "Lambda functions has the required permission to write CloudWatch Logs. It uses custom policy instead of arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole with tighter permissions." + }, + { + "id": "W89", + "reason": "This is not a rule for the general case, just for specific use cases/industries" + }, + { + "id": "W92", + "reason": "Impossible for us to define the correct concurrency for clients" + } + ] + } + } + }, + "testlambdaopensearchCognitoUserPoolA09096F9": { + "Type": "AWS::Cognito::UserPool", + "Properties": { + "AccountRecoverySetting": { + "RecoveryMechanisms": [ + { + "Name": "verified_phone_number", + "Priority": 1 + }, + { + "Name": "verified_email", + "Priority": 2 + } + ] + }, + "AdminCreateUserConfig": { + "AllowAdminCreateUserOnly": true + }, + "EmailVerificationMessage": "The verification code to your new account is {####}", + "EmailVerificationSubject": "Verify your new account", + "SmsVerificationMessage": "The verification code to your new account is {####}", + "UserPoolAddOns": { + "AdvancedSecurityMode": "ENFORCED" + }, + "VerificationMessageTemplate": { + "DefaultEmailOption": "CONFIRM_WITH_CODE", + "EmailMessage": "The verification code to your new account is {####}", + "EmailSubject": "Verify your new account", + "SmsMessage": "The verification code to your new account is {####}" + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "testlambdaopensearchCognitoUserPoolClient39C21D94": { + "Type": "AWS::Cognito::UserPoolClient", + "Properties": { + "UserPoolId": { + "Ref": "testlambdaopensearchCognitoUserPoolA09096F9" + }, + "AllowedOAuthFlows": [ + "implicit", + "code" + ], + "AllowedOAuthFlowsUserPoolClient": true, + "AllowedOAuthScopes": [ + "profile", + "phone", + "email", + "openid", + "aws.cognito.signin.user.admin" + ], + "CallbackURLs": [ + "https://example.com" + ], + "SupportedIdentityProviders": [ + "COGNITO" + ] + } + }, + "testlambdaopensearchCognitoIdentityPool0B1FB311": { + "Type": "AWS::Cognito::IdentityPool", + "Properties": { + "AllowUnauthenticatedIdentities": false, + "CognitoIdentityProviders": [ + { + "ClientId": { + "Ref": "testlambdaopensearchCognitoUserPoolClient39C21D94" + }, + "ProviderName": { + "Fn::GetAtt": [ + "testlambdaopensearchCognitoUserPoolA09096F9", + "ProviderName" + ] + }, + "ServerSideTokenCheck": true + } + ] + } + }, + "testlambdaopensearchUserPoolDomain98864920": { + "Type": "AWS::Cognito::UserPoolDomain", + "Properties": { + "Domain": "cogn-solutions-constructs-domain", + "UserPoolId": { + "Ref": "testlambdaopensearchCognitoUserPoolA09096F9" + } + }, + "DependsOn": [ + "testlambdaopensearchCognitoUserPoolA09096F9" + ] + }, + "testlambdaopensearchCognitoAuthorizedRole58A1ED44": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRoleWithWebIdentity", + "Condition": { + "StringEquals": { + "cognito-identity.amazonaws.com:aud": { + "Ref": "testlambdaopensearchCognitoIdentityPool0B1FB311" + } + }, + "ForAnyValue:StringLike": { + "cognito-identity.amazonaws.com:amr": "authenticated" + } + }, + "Effect": "Allow", + "Principal": { + "Federated": "cognito-identity.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": "es:ESHttp*", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":es:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":domain/cogn-solutions-constructs-domain/*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "CognitoAccessPolicy" + } + ] + } + }, + "testlambdaopensearchIdentityPoolRoleMappingD8C765B1": { + "Type": "AWS::Cognito::IdentityPoolRoleAttachment", + "Properties": { + "IdentityPoolId": { + "Ref": "testlambdaopensearchCognitoIdentityPool0B1FB311" + }, + "Roles": { + "authenticated": { + "Fn::GetAtt": [ + "testlambdaopensearchCognitoAuthorizedRole58A1ED44", + "Arn" + ] + } + } + } + }, + "testlambdaopensearchCognitoKibanaConfigureRole9623271F": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "es.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "testlambdaopensearchCognitoKibanaConfigureRolePolicyE9D9C211": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "cognito-idp:DescribeUserPool", + "cognito-idp:CreateUserPoolClient", + "cognito-idp:DeleteUserPoolClient", + "cognito-idp:DescribeUserPoolClient", + "cognito-idp:AdminInitiateAuth", + "cognito-idp:AdminUserGlobalSignOut", + "cognito-idp:ListUserPoolClients", + "cognito-identity:DescribeIdentityPool", + "cognito-identity:UpdateIdentityPool", + "cognito-identity:SetIdentityPoolRoles", + "cognito-identity:GetIdentityPoolRoles", + "es:UpdateDomainConfig" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "testlambdaopensearchCognitoUserPoolA09096F9", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:aws:cognito-identity:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":identitypool/", + { + "Ref": "testlambdaopensearchCognitoIdentityPool0B1FB311" + } + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:aws:es:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":domain/solutions-constructs-domain" + ] + ] + } + ] + }, + { + "Action": "iam:PassRole", + "Condition": { + "StringLike": { + "iam:PassedToService": "cognito-identity.amazonaws.com" + } + }, + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "testlambdaopensearchCognitoKibanaConfigureRole9623271F", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "testlambdaopensearchCognitoKibanaConfigureRolePolicyE9D9C211", + "Roles": [ + { + "Ref": "testlambdaopensearchCognitoKibanaConfigureRole9623271F" + } + ] + } + }, + "testlambdaopensearchOpenSearchDomainF9CCC3D3": { + "Type": "AWS::OpenSearchService::Domain", + "Properties": { + "AccessPolicies": { + "Statement": [ + { + "Action": "es:ESHttp*", + "Effect": "Allow", + "Principal": { + "AWS": [ + { + "Fn::GetAtt": [ + "testlambdaopensearchCognitoAuthorizedRole58A1ED44", + "Arn" + ] + }, + { + "Fn::GetAtt": [ + "testlambdaopensearchLambdaFunctionServiceRole4722AB8A", + "Arn" + ] + } + ] + }, + "Resource": { + "Fn::Join": [ + "", + [ + "arn:aws:es:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":domain/solutions-constructs-domain/*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "ClusterConfig": { + "DedicatedMasterCount": 3, + "DedicatedMasterEnabled": true, + "InstanceCount": 3, + "ZoneAwarenessConfig": { + "AvailabilityZoneCount": 3 + }, + "ZoneAwarenessEnabled": true + }, + "CognitoOptions": { + "Enabled": true, + "IdentityPoolId": { + "Ref": "testlambdaopensearchCognitoIdentityPool0B1FB311" + }, + "RoleArn": { + "Fn::GetAtt": [ + "testlambdaopensearchCognitoKibanaConfigureRole9623271F", + "Arn" + ] + }, + "UserPoolId": { + "Ref": "testlambdaopensearchCognitoUserPoolA09096F9" + } + }, + "DomainName": "solutions-constructs-domain", + "EBSOptions": { + "EBSEnabled": true, + "VolumeSize": 10 + }, + "EncryptionAtRestOptions": { + "Enabled": true + }, + "EngineVersion": "OpenSearch_1.3", + "NodeToNodeEncryptionOptions": { + "Enabled": true + }, + "SnapshotOptions": { + "AutomatedSnapshotStartHour": 1 + } + }, + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W28", + "reason": "The OpenSearch Service domain is passed dynamically as as parameter and explicitly specified to ensure that IAM policies are configured to lockdown access to this specific OpenSearch Service instance only" + }, + { + "id": "W90", + "reason": "This is not a rule for the general case, just for specific use cases/industries" + } + ] + } + } + }, + "testlambdaopensearchStatusRedAlarm1627144D": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "AlarmDescription": "At least one primary shard and its replicas are not allocated to a node. ", + "MetricName": "ClusterStatus.red", + "Namespace": "AWS/ES", + "Period": 60, + "Statistic": "Maximum", + "Threshold": 1 + } + }, + "testlambdaopensearchStatusYellowAlarm57139CF0": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "AlarmDescription": "At least one replica shard is not allocated to a node.", + "MetricName": "ClusterStatus.yellow", + "Namespace": "AWS/ES", + "Period": 60, + "Statistic": "Maximum", + "Threshold": 1 + } + }, + "testlambdaopensearchFreeStorageSpaceTooLowAlarm6A5E1E96": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "LessThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "AlarmDescription": "A node in your cluster is down to 20 GiB of free storage space.", + "MetricName": "FreeStorageSpace", + "Namespace": "AWS/ES", + "Period": 60, + "Statistic": "Minimum", + "Threshold": 20000 + } + }, + "testlambdaopensearchIndexWritesBlockedTooHighAlarmD2E041A3": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "AlarmDescription": "Your cluster is blocking write requests.", + "MetricName": "ClusterIndexWritesBlocked", + "Namespace": "AWS/ES", + "Period": 300, + "Statistic": "Maximum", + "Threshold": 1 + } + }, + "testlambdaopensearchAutomatedSnapshotFailureTooHighAlarm9A4D0B1F": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "AlarmDescription": "An automated snapshot failed. This failure is often the result of a red cluster health status.", + "MetricName": "AutomatedSnapshotFailure", + "Namespace": "AWS/ES", + "Period": 60, + "Statistic": "Maximum", + "Threshold": 1 + } + }, + "testlambdaopensearchCPUUtilizationTooHighAlarmC4850758": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 3, + "AlarmDescription": "100% CPU utilization is not uncommon, but sustained high usage is problematic. Consider using larger instance types or adding instances.", + "MetricName": "CPUUtilization", + "Namespace": "AWS/ES", + "Period": 900, + "Statistic": "Average", + "Threshold": 80 + } + }, + "testlambdaopensearchJVMMemoryPressureTooHighAlarmEFB09A7C": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "AlarmDescription": "Average JVM memory pressure over last 15 minutes too high. Consider scaling vertically.", + "MetricName": "JVMMemoryPressure", + "Namespace": "AWS/ES", + "Period": 900, + "Statistic": "Average", + "Threshold": 80 + } + }, + "testlambdaopensearchMasterCPUUtilizationTooHighAlarm124D5748": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 3, + "AlarmDescription": "Average CPU utilization over last 45 minutes too high. Consider using larger instance types for your dedicated master nodes.", + "MetricName": "MasterCPUUtilization", + "Namespace": "AWS/ES", + "Period": 900, + "Statistic": "Average", + "Threshold": 50 + } + }, + "testlambdaopensearchMasterJVMMemoryPressureTooHighAlarmBC9524D3": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "AlarmDescription": "Average JVM memory pressure over last 15 minutes too high. Consider scaling vertically.", + "MetricName": "MasterJVMMemoryPressure", + "Namespace": "AWS/ES", + "Period": 900, + "Statistic": "Average", + "Threshold": 50 + } + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/test/integ.domain-arguments.ts b/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/test/integ.domain-arguments.ts new file mode 100644 index 000000000..7d503e934 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/test/integ.domain-arguments.ts @@ -0,0 +1,40 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +/// !cdk-integ * +import { App, Stack } from "aws-cdk-lib"; +import { LambdaToOpenSearch } from "../lib"; +import * as lambda from 'aws-cdk-lib/aws-lambda'; +import { generateIntegStackName } from '@aws-solutions-constructs/core'; + +// Setup +const app = new App(); +const stack = new Stack(app, generateIntegStackName(__filename)); + +const lambdaProps: lambda.FunctionProps = { + code: lambda.Code.fromAsset(`${__dirname}/lambda`), + runtime: lambda.Runtime.NODEJS_14_X, + handler: 'index.handler' +}; + +const openSearchDomain = 'solutions-constructs-domain'; +const cognitoDomain = 'cogn-solutions-constructs-domain'; + +new LambdaToOpenSearch(stack, 'test-lambda-opensearch', { + lambdaFunctionProps: lambdaProps, + openSearchDomainName: openSearchDomain, + cognitoDomainName: cognitoDomain +}); + +// Synth +app.synth(); diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/test/integ.existing-vpc.expected.json b/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/test/integ.existing-vpc.expected.json new file mode 100644 index 000000000..b21beaacc --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/test/integ.existing-vpc.expected.json @@ -0,0 +1,1431 @@ +{ + "Resources": { + "Vpc8378EB38": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "172.168.0.0/16", + "EnableDnsHostnames": true, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": [ + { + "Key": "Name", + "Value": "existing-vpc/Vpc" + } + ] + } + }, + "VpcPublicSubnet1Subnet5C2D37C4": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1a", + "CidrBlock": "172.168.0.0/19", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "existing-vpc/Vpc/PublicSubnet1" + } + ] + }, + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W33", + "reason": "Allow Public Subnets to have MapPublicIpOnLaunch set to true" + } + ] + } + } + }, + "VpcPublicSubnet1RouteTable6C95E38E": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "existing-vpc/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet1RouteTableAssociation97140677": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet1RouteTable6C95E38E" + }, + "SubnetId": { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4" + } + } + }, + "VpcPublicSubnet1DefaultRoute3DA9E72A": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet1RouteTable6C95E38E" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcIGWD7BA715C" + } + }, + "DependsOn": [ + "VpcVPCGWBF912B6E" + ] + }, + "VpcPublicSubnet1EIPD7E02669": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "existing-vpc/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet1NATGateway4D7517AA": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "SubnetId": { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4" + }, + "AllocationId": { + "Fn::GetAtt": [ + "VpcPublicSubnet1EIPD7E02669", + "AllocationId" + ] + }, + "Tags": [ + { + "Key": "Name", + "Value": "existing-vpc/Vpc/PublicSubnet1" + } + ] + }, + "DependsOn": [ + "VpcPublicSubnet1DefaultRoute3DA9E72A", + "VpcPublicSubnet1RouteTableAssociation97140677" + ] + }, + "VpcPublicSubnet2Subnet691E08A3": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1b", + "CidrBlock": "172.168.32.0/19", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "existing-vpc/Vpc/PublicSubnet2" + } + ] + }, + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W33", + "reason": "Allow Public Subnets to have MapPublicIpOnLaunch set to true" + } + ] + } + } + }, + "VpcPublicSubnet2RouteTable94F7E489": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "existing-vpc/Vpc/PublicSubnet2" + } + ] + } + }, + "VpcPublicSubnet2RouteTableAssociationDD5762D8": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet2RouteTable94F7E489" + }, + "SubnetId": { + "Ref": "VpcPublicSubnet2Subnet691E08A3" + } + } + }, + "VpcPublicSubnet2DefaultRoute97F91067": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet2RouteTable94F7E489" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcIGWD7BA715C" + } + }, + "DependsOn": [ + "VpcVPCGWBF912B6E" + ] + }, + "VpcPublicSubnet2EIP3C605A87": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "existing-vpc/Vpc/PublicSubnet2" + } + ] + } + }, + "VpcPublicSubnet2NATGateway9182C01D": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "SubnetId": { + "Ref": "VpcPublicSubnet2Subnet691E08A3" + }, + "AllocationId": { + "Fn::GetAtt": [ + "VpcPublicSubnet2EIP3C605A87", + "AllocationId" + ] + }, + "Tags": [ + { + "Key": "Name", + "Value": "existing-vpc/Vpc/PublicSubnet2" + } + ] + }, + "DependsOn": [ + "VpcPublicSubnet2DefaultRoute97F91067", + "VpcPublicSubnet2RouteTableAssociationDD5762D8" + ] + }, + "VpcPublicSubnet3SubnetBE12F0B6": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1c", + "CidrBlock": "172.168.64.0/19", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "existing-vpc/Vpc/PublicSubnet3" + } + ] + }, + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W33", + "reason": "Allow Public Subnets to have MapPublicIpOnLaunch set to true" + } + ] + } + } + }, + "VpcPublicSubnet3RouteTable93458DBB": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "existing-vpc/Vpc/PublicSubnet3" + } + ] + } + }, + "VpcPublicSubnet3RouteTableAssociation1F1EDF02": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet3RouteTable93458DBB" + }, + "SubnetId": { + "Ref": "VpcPublicSubnet3SubnetBE12F0B6" + } + } + }, + "VpcPublicSubnet3DefaultRoute4697774F": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet3RouteTable93458DBB" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcIGWD7BA715C" + } + }, + "DependsOn": [ + "VpcVPCGWBF912B6E" + ] + }, + "VpcPublicSubnet3EIP3A666A23": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "existing-vpc/Vpc/PublicSubnet3" + } + ] + } + }, + "VpcPublicSubnet3NATGateway7640CD1D": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "SubnetId": { + "Ref": "VpcPublicSubnet3SubnetBE12F0B6" + }, + "AllocationId": { + "Fn::GetAtt": [ + "VpcPublicSubnet3EIP3A666A23", + "AllocationId" + ] + }, + "Tags": [ + { + "Key": "Name", + "Value": "existing-vpc/Vpc/PublicSubnet3" + } + ] + }, + "DependsOn": [ + "VpcPublicSubnet3DefaultRoute4697774F", + "VpcPublicSubnet3RouteTableAssociation1F1EDF02" + ] + }, + "VpcPrivateSubnet1Subnet536B997A": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1a", + "CidrBlock": "172.168.96.0/19", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "existing-vpc/Vpc/PrivateSubnet1" + } + ] + } + }, + "VpcPrivateSubnet1RouteTableB2C5B500": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "existing-vpc/Vpc/PrivateSubnet1" + } + ] + } + }, + "VpcPrivateSubnet1RouteTableAssociation70C59FA6": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" + }, + "SubnetId": { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + } + } + }, + "VpcPrivateSubnet1DefaultRouteBE02A9ED": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VpcPublicSubnet1NATGateway4D7517AA" + } + } + }, + "VpcPrivateSubnet2Subnet3788AAA1": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1b", + "CidrBlock": "172.168.128.0/19", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "existing-vpc/Vpc/PrivateSubnet2" + } + ] + } + }, + "VpcPrivateSubnet2RouteTableA678073B": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "existing-vpc/Vpc/PrivateSubnet2" + } + ] + } + }, + "VpcPrivateSubnet2RouteTableAssociationA89CAD56": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet2RouteTableA678073B" + }, + "SubnetId": { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + } + } + }, + "VpcPrivateSubnet2DefaultRoute060D2087": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet2RouteTableA678073B" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VpcPublicSubnet2NATGateway9182C01D" + } + } + }, + "VpcPrivateSubnet3SubnetF258B56E": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1c", + "CidrBlock": "172.168.160.0/19", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "existing-vpc/Vpc/PrivateSubnet3" + } + ] + } + }, + "VpcPrivateSubnet3RouteTableD98824C7": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "existing-vpc/Vpc/PrivateSubnet3" + } + ] + } + }, + "VpcPrivateSubnet3RouteTableAssociation16BDDC43": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet3RouteTableD98824C7" + }, + "SubnetId": { + "Ref": "VpcPrivateSubnet3SubnetF258B56E" + } + } + }, + "VpcPrivateSubnet3DefaultRoute94B74F0D": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet3RouteTableD98824C7" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VpcPublicSubnet3NATGateway7640CD1D" + } + } + }, + "VpcIGWD7BA715C": { + "Type": "AWS::EC2::InternetGateway", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "existing-vpc/Vpc" + } + ] + } + }, + "VpcVPCGWBF912B6E": { + "Type": "AWS::EC2::VPCGatewayAttachment", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "InternetGatewayId": { + "Ref": "VpcIGWD7BA715C" + } + } + }, + "VpcFlowLogIAMRole6A475D41": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "vpc-flow-logs.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Tags": [ + { + "Key": "Name", + "Value": "existing-vpc/Vpc" + } + ] + } + }, + "VpcFlowLogIAMRoleDefaultPolicy406FB995": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogStream", + "logs:PutLogEvents", + "logs:DescribeLogStreams" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "VpcFlowLogLogGroup7B5C56B9", + "Arn" + ] + } + }, + { + "Action": "iam:PassRole", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "VpcFlowLogIAMRole6A475D41", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "VpcFlowLogIAMRoleDefaultPolicy406FB995", + "Roles": [ + { + "Ref": "VpcFlowLogIAMRole6A475D41" + } + ] + } + }, + "VpcFlowLogLogGroup7B5C56B9": { + "Type": "AWS::Logs::LogGroup", + "Properties": { + "RetentionInDays": 731, + "Tags": [ + { + "Key": "Name", + "Value": "existing-vpc/Vpc" + } + ] + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain", + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W84", + "reason": "By default CloudWatchLogs LogGroups data is encrypted using the CloudWatch server-side encryption keys (AWS Managed Keys)" + } + ] + } + } + }, + "VpcFlowLog8FF33A73": { + "Type": "AWS::EC2::FlowLog", + "Properties": { + "ResourceId": { + "Ref": "Vpc8378EB38" + }, + "ResourceType": "VPC", + "TrafficType": "ALL", + "DeliverLogsPermissionArn": { + "Fn::GetAtt": [ + "VpcFlowLogIAMRole6A475D41", + "Arn" + ] + }, + "LogDestinationType": "cloud-watch-logs", + "LogGroupName": { + "Ref": "VpcFlowLogLogGroup7B5C56B9" + }, + "Tags": [ + { + "Key": "Name", + "Value": "existing-vpc/Vpc" + } + ] + } + }, + "testlambdaelasticsearchkibana4LambdaFunctionServiceRoleA52BB7F9": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:/aws/lambda/*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "LambdaFunctionServiceRolePolicy" + } + ] + } + }, + "testlambdaelasticsearchkibana4LambdaFunctionServiceRoleDefaultPolicyA5AD88E5": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "ec2:CreateNetworkInterface", + "ec2:DescribeNetworkInterfaces", + "ec2:DeleteNetworkInterface", + "ec2:AssignPrivateIpAddresses", + "ec2:UnassignPrivateIpAddresses" + ], + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "xray:PutTraceSegments", + "xray:PutTelemetryRecords" + ], + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "testlambdaelasticsearchkibana4LambdaFunctionServiceRoleDefaultPolicyA5AD88E5", + "Roles": [ + { + "Ref": "testlambdaelasticsearchkibana4LambdaFunctionServiceRoleA52BB7F9" + } + ] + }, + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W12", + "reason": "Lambda needs the following minimum required permissions to send trace data to X-Ray and access ENIs in a VPC." + } + ] + } + } + }, + "testlambdaelasticsearchkibana4ReplaceDefaultSecurityGroupsecuritygroupA79E2B92": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "existing-vpc/test-lambda-elasticsearch-kibana4/ReplaceDefaultSecurityGroup-security-group", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "VpcId": { + "Ref": "Vpc8378EB38" + } + }, + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W5", + "reason": "Egress of 0.0.0.0/0 is default and generally considered OK" + }, + { + "id": "W40", + "reason": "Egress IPProtocol of -1 is default and generally considered OK" + } + ] + } + } + }, + "testlambdaelasticsearchkibana4LambdaFunction2C5856DF": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-12345678-test-region", + "S3Key": "466cbfc6653782bd2707dec5bc5c58005c75a8bff3660a58eefa57d667120240.zip" + }, + "Role": { + "Fn::GetAtt": [ + "testlambdaelasticsearchkibana4LambdaFunctionServiceRoleA52BB7F9", + "Arn" + ] + }, + "Environment": { + "Variables": { + "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1", + "DOMAIN_ENDPOINT": { + "Fn::GetAtt": [ + "testlambdaelasticsearchkibana4OpenSearchDomain94EAD3A3", + "DomainEndpoint" + ] + } + } + }, + "Handler": "index.handler", + "Runtime": "nodejs14.x", + "TracingConfig": { + "Mode": "Active" + }, + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::GetAtt": [ + "testlambdaelasticsearchkibana4ReplaceDefaultSecurityGroupsecuritygroupA79E2B92", + "GroupId" + ] + } + ], + "SubnetIds": [ + { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + }, + { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + }, + { + "Ref": "VpcPrivateSubnet3SubnetF258B56E" + } + ] + } + }, + "DependsOn": [ + "testlambdaelasticsearchkibana4LambdaFunctionServiceRoleDefaultPolicyA5AD88E5", + "testlambdaelasticsearchkibana4LambdaFunctionServiceRoleA52BB7F9", + "VpcPrivateSubnet1DefaultRouteBE02A9ED", + "VpcPrivateSubnet1RouteTableAssociation70C59FA6", + "VpcPrivateSubnet2DefaultRoute060D2087", + "VpcPrivateSubnet2RouteTableAssociationA89CAD56", + "VpcPrivateSubnet3DefaultRoute94B74F0D", + "VpcPrivateSubnet3RouteTableAssociation16BDDC43" + ], + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W58", + "reason": "Lambda functions has the required permission to write CloudWatch Logs. It uses custom policy instead of arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole with tighter permissions." + }, + { + "id": "W89", + "reason": "This is not a rule for the general case, just for specific use cases/industries" + }, + { + "id": "W92", + "reason": "Impossible for us to define the correct concurrency for clients" + } + ] + } + } + }, + "testlambdaelasticsearchkibana4CognitoUserPool37A5CDE1": { + "Type": "AWS::Cognito::UserPool", + "Properties": { + "AccountRecoverySetting": { + "RecoveryMechanisms": [ + { + "Name": "verified_phone_number", + "Priority": 1 + }, + { + "Name": "verified_email", + "Priority": 2 + } + ] + }, + "AdminCreateUserConfig": { + "AllowAdminCreateUserOnly": true + }, + "EmailVerificationMessage": "The verification code to your new account is {####}", + "EmailVerificationSubject": "Verify your new account", + "SmsVerificationMessage": "The verification code to your new account is {####}", + "UserPoolAddOns": { + "AdvancedSecurityMode": "ENFORCED" + }, + "VerificationMessageTemplate": { + "DefaultEmailOption": "CONFIRM_WITH_CODE", + "EmailMessage": "The verification code to your new account is {####}", + "EmailSubject": "Verify your new account", + "SmsMessage": "The verification code to your new account is {####}" + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "testlambdaelasticsearchkibana4CognitoUserPoolClientABBF34C4": { + "Type": "AWS::Cognito::UserPoolClient", + "Properties": { + "UserPoolId": { + "Ref": "testlambdaelasticsearchkibana4CognitoUserPool37A5CDE1" + }, + "AllowedOAuthFlows": [ + "implicit", + "code" + ], + "AllowedOAuthFlowsUserPoolClient": true, + "AllowedOAuthScopes": [ + "profile", + "phone", + "email", + "openid", + "aws.cognito.signin.user.admin" + ], + "CallbackURLs": [ + "https://example.com" + ], + "SupportedIdentityProviders": [ + "COGNITO" + ] + } + }, + "testlambdaelasticsearchkibana4CognitoIdentityPool76EE9793": { + "Type": "AWS::Cognito::IdentityPool", + "Properties": { + "AllowUnauthenticatedIdentities": false, + "CognitoIdentityProviders": [ + { + "ClientId": { + "Ref": "testlambdaelasticsearchkibana4CognitoUserPoolClientABBF34C4" + }, + "ProviderName": { + "Fn::GetAtt": [ + "testlambdaelasticsearchkibana4CognitoUserPool37A5CDE1", + "ProviderName" + ] + }, + "ServerSideTokenCheck": true + } + ] + } + }, + "testlambdaelasticsearchkibana4UserPoolDomain4CAAF2F6": { + "Type": "AWS::Cognito::UserPoolDomain", + "Properties": { + "Domain": "deploytestwithexistingvpc", + "UserPoolId": { + "Ref": "testlambdaelasticsearchkibana4CognitoUserPool37A5CDE1" + } + }, + "DependsOn": [ + "testlambdaelasticsearchkibana4CognitoUserPool37A5CDE1" + ] + }, + "testlambdaelasticsearchkibana4CognitoAuthorizedRoleA7D6B392": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRoleWithWebIdentity", + "Condition": { + "StringEquals": { + "cognito-identity.amazonaws.com:aud": { + "Ref": "testlambdaelasticsearchkibana4CognitoIdentityPool76EE9793" + } + }, + "ForAnyValue:StringLike": { + "cognito-identity.amazonaws.com:amr": "authenticated" + } + }, + "Effect": "Allow", + "Principal": { + "Federated": "cognito-identity.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": "es:ESHttp*", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":es:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":domain/deploytestwithexistingvpc/*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "CognitoAccessPolicy" + } + ] + } + }, + "testlambdaelasticsearchkibana4IdentityPoolRoleMapping9378D177": { + "Type": "AWS::Cognito::IdentityPoolRoleAttachment", + "Properties": { + "IdentityPoolId": { + "Ref": "testlambdaelasticsearchkibana4CognitoIdentityPool76EE9793" + }, + "Roles": { + "authenticated": { + "Fn::GetAtt": [ + "testlambdaelasticsearchkibana4CognitoAuthorizedRoleA7D6B392", + "Arn" + ] + } + } + } + }, + "testlambdaelasticsearchkibana4CognitoKibanaConfigureRole6A058B80": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "es.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "testlambdaelasticsearchkibana4CognitoKibanaConfigureRolePolicy1615E937": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "cognito-idp:DescribeUserPool", + "cognito-idp:CreateUserPoolClient", + "cognito-idp:DeleteUserPoolClient", + "cognito-idp:DescribeUserPoolClient", + "cognito-idp:AdminInitiateAuth", + "cognito-idp:AdminUserGlobalSignOut", + "cognito-idp:ListUserPoolClients", + "cognito-identity:DescribeIdentityPool", + "cognito-identity:UpdateIdentityPool", + "cognito-identity:SetIdentityPoolRoles", + "cognito-identity:GetIdentityPoolRoles", + "es:UpdateDomainConfig" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "testlambdaelasticsearchkibana4CognitoUserPool37A5CDE1", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:aws:cognito-identity:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":identitypool/", + { + "Ref": "testlambdaelasticsearchkibana4CognitoIdentityPool76EE9793" + } + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:aws:es:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":domain/deploytestwithexistingvpc" + ] + ] + } + ] + }, + { + "Action": "iam:PassRole", + "Condition": { + "StringLike": { + "iam:PassedToService": "cognito-identity.amazonaws.com" + } + }, + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "testlambdaelasticsearchkibana4CognitoKibanaConfigureRole6A058B80", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "testlambdaelasticsearchkibana4CognitoKibanaConfigureRolePolicy1615E937", + "Roles": [ + { + "Ref": "testlambdaelasticsearchkibana4CognitoKibanaConfigureRole6A058B80" + } + ] + } + }, + "testlambdaelasticsearchkibana4OpenSearchDomain94EAD3A3": { + "Type": "AWS::OpenSearchService::Domain", + "Properties": { + "AccessPolicies": { + "Statement": [ + { + "Action": "es:ESHttp*", + "Effect": "Allow", + "Principal": { + "AWS": [ + { + "Fn::GetAtt": [ + "testlambdaelasticsearchkibana4CognitoAuthorizedRoleA7D6B392", + "Arn" + ] + }, + { + "Fn::GetAtt": [ + "testlambdaelasticsearchkibana4LambdaFunctionServiceRoleA52BB7F9", + "Arn" + ] + } + ] + }, + "Resource": { + "Fn::Join": [ + "", + [ + "arn:aws:es:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":domain/deploytestwithexistingvpc/*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "ClusterConfig": { + "DedicatedMasterCount": 3, + "DedicatedMasterEnabled": true, + "InstanceCount": 3, + "ZoneAwarenessConfig": { + "AvailabilityZoneCount": 3 + }, + "ZoneAwarenessEnabled": true + }, + "CognitoOptions": { + "Enabled": true, + "IdentityPoolId": { + "Ref": "testlambdaelasticsearchkibana4CognitoIdentityPool76EE9793" + }, + "RoleArn": { + "Fn::GetAtt": [ + "testlambdaelasticsearchkibana4CognitoKibanaConfigureRole6A058B80", + "Arn" + ] + }, + "UserPoolId": { + "Ref": "testlambdaelasticsearchkibana4CognitoUserPool37A5CDE1" + } + }, + "DomainName": "deploytestwithexistingvpc", + "EBSOptions": { + "EBSEnabled": true, + "VolumeSize": 10 + }, + "EncryptionAtRestOptions": { + "Enabled": true + }, + "EngineVersion": "OpenSearch_1.3", + "NodeToNodeEncryptionOptions": { + "Enabled": true + }, + "SnapshotOptions": { + "AutomatedSnapshotStartHour": 1 + }, + "VPCOptions": { + "SecurityGroupIds": [ + { + "Fn::GetAtt": [ + "testlambdaelasticsearchkibana4ReplaceDefaultSecurityGroupsecuritygroupA79E2B92", + "GroupId" + ] + } + ], + "SubnetIds": [ + { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + }, + { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + }, + { + "Ref": "VpcPrivateSubnet3SubnetF258B56E" + } + ] + } + }, + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W28", + "reason": "The OpenSearch Service domain is passed dynamically as as parameter and explicitly specified to ensure that IAM policies are configured to lockdown access to this specific OpenSearch Service instance only" + }, + { + "id": "W90", + "reason": "This is not a rule for the general case, just for specific use cases/industries" + } + ] + } + } + }, + "testlambdaelasticsearchkibana4StatusRedAlarm56DEE5C7": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "AlarmDescription": "At least one primary shard and its replicas are not allocated to a node. ", + "MetricName": "ClusterStatus.red", + "Namespace": "AWS/ES", + "Period": 60, + "Statistic": "Maximum", + "Threshold": 1 + } + }, + "testlambdaelasticsearchkibana4StatusYellowAlarm810B4F9E": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "AlarmDescription": "At least one replica shard is not allocated to a node.", + "MetricName": "ClusterStatus.yellow", + "Namespace": "AWS/ES", + "Period": 60, + "Statistic": "Maximum", + "Threshold": 1 + } + }, + "testlambdaelasticsearchkibana4FreeStorageSpaceTooLowAlarmF3FB31EA": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "LessThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "AlarmDescription": "A node in your cluster is down to 20 GiB of free storage space.", + "MetricName": "FreeStorageSpace", + "Namespace": "AWS/ES", + "Period": 60, + "Statistic": "Minimum", + "Threshold": 20000 + } + }, + "testlambdaelasticsearchkibana4IndexWritesBlockedTooHighAlarmF2968C92": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "AlarmDescription": "Your cluster is blocking write requests.", + "MetricName": "ClusterIndexWritesBlocked", + "Namespace": "AWS/ES", + "Period": 300, + "Statistic": "Maximum", + "Threshold": 1 + } + }, + "testlambdaelasticsearchkibana4AutomatedSnapshotFailureTooHighAlarm53EB1ABB": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "AlarmDescription": "An automated snapshot failed. This failure is often the result of a red cluster health status.", + "MetricName": "AutomatedSnapshotFailure", + "Namespace": "AWS/ES", + "Period": 60, + "Statistic": "Maximum", + "Threshold": 1 + } + }, + "testlambdaelasticsearchkibana4CPUUtilizationTooHighAlarm29B67D10": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 3, + "AlarmDescription": "100% CPU utilization is not uncommon, but sustained high usage is problematic. Consider using larger instance types or adding instances.", + "MetricName": "CPUUtilization", + "Namespace": "AWS/ES", + "Period": 900, + "Statistic": "Average", + "Threshold": 80 + } + }, + "testlambdaelasticsearchkibana4JVMMemoryPressureTooHighAlarm9DDED711": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "AlarmDescription": "Average JVM memory pressure over last 15 minutes too high. Consider scaling vertically.", + "MetricName": "JVMMemoryPressure", + "Namespace": "AWS/ES", + "Period": 900, + "Statistic": "Average", + "Threshold": 80 + } + }, + "testlambdaelasticsearchkibana4MasterCPUUtilizationTooHighAlarmE66867F2": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 3, + "AlarmDescription": "Average CPU utilization over last 45 minutes too high. Consider using larger instance types for your dedicated master nodes.", + "MetricName": "MasterCPUUtilization", + "Namespace": "AWS/ES", + "Period": 900, + "Statistic": "Average", + "Threshold": 50 + } + }, + "testlambdaelasticsearchkibana4MasterJVMMemoryPressureTooHighAlarm83E1822E": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "AlarmDescription": "Average JVM memory pressure over last 15 minutes too high. Consider scaling vertically.", + "MetricName": "MasterJVMMemoryPressure", + "Namespace": "AWS/ES", + "Period": 900, + "Statistic": "Average", + "Threshold": 50 + } + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/test/integ.existing-vpc.ts b/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/test/integ.existing-vpc.ts new file mode 100644 index 000000000..7a40cced4 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/test/integ.existing-vpc.ts @@ -0,0 +1,44 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +/// !cdk-integ * +import { App, Stack } from "aws-cdk-lib"; +import { LambdaToOpenSearch } from "../lib"; +import * as lambda from 'aws-cdk-lib/aws-lambda'; +import * as defaults from '@aws-solutions-constructs/core'; + +const app = new App(); +const stack = new Stack(app, defaults.generateIntegStackName(__filename), { + env: { + region: process.env.CDK_DEFAULT_REGION, + account: process.env.CDK_DEFAULT_ACCOUNT, + } +}); + +// Create VPC +const vpc = defaults.getTestVpc(stack); + +const lambdaProps: lambda.FunctionProps = { + code: lambda.Code.fromAsset(`lambda`), + runtime: lambda.Runtime.NODEJS_14_X, + handler: 'index.handler', +}; + +new LambdaToOpenSearch(stack, 'test-lambda-elasticsearch-kibana4', { + lambdaFunctionProps: lambdaProps, + openSearchDomainName: "deploytestwithexistingvpc", + existingVpc: vpc +}); + +// Synth +app.synth(); \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/test/integ.no-arguments.expected.json b/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/test/integ.no-arguments.expected.json new file mode 100644 index 000000000..b65887f99 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/test/integ.no-arguments.expected.json @@ -0,0 +1,675 @@ +{ + "Resources": { + "testlambdaopensearchLambdaFunctionServiceRole4722AB8A": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:/aws/lambda/*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "LambdaFunctionServiceRolePolicy" + } + ] + } + }, + "testlambdaopensearchLambdaFunctionServiceRoleDefaultPolicy78C56359": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "xray:PutTraceSegments", + "xray:PutTelemetryRecords" + ], + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "testlambdaopensearchLambdaFunctionServiceRoleDefaultPolicy78C56359", + "Roles": [ + { + "Ref": "testlambdaopensearchLambdaFunctionServiceRole4722AB8A" + } + ] + }, + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W12", + "reason": "Lambda needs the following minimum required permissions to send trace data to X-Ray and access ENIs in a VPC." + } + ] + } + } + }, + "testlambdaopensearchLambdaFunction93FD38F7": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" + }, + "S3Key": "466cbfc6653782bd2707dec5bc5c58005c75a8bff3660a58eefa57d667120240.zip" + }, + "Role": { + "Fn::GetAtt": [ + "testlambdaopensearchLambdaFunctionServiceRole4722AB8A", + "Arn" + ] + }, + "Environment": { + "Variables": { + "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1", + "DOMAIN_ENDPOINT": { + "Fn::GetAtt": [ + "testlambdaopensearchOpenSearchDomainF9CCC3D3", + "DomainEndpoint" + ] + } + } + }, + "Handler": "index.handler", + "Runtime": "nodejs14.x", + "TracingConfig": { + "Mode": "Active" + } + }, + "DependsOn": [ + "testlambdaopensearchLambdaFunctionServiceRoleDefaultPolicy78C56359", + "testlambdaopensearchLambdaFunctionServiceRole4722AB8A" + ], + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W58", + "reason": "Lambda functions has the required permission to write CloudWatch Logs. It uses custom policy instead of arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole with tighter permissions." + }, + { + "id": "W89", + "reason": "This is not a rule for the general case, just for specific use cases/industries" + }, + { + "id": "W92", + "reason": "Impossible for us to define the correct concurrency for clients" + } + ] + } + } + }, + "testlambdaopensearchCognitoUserPoolA09096F9": { + "Type": "AWS::Cognito::UserPool", + "Properties": { + "AccountRecoverySetting": { + "RecoveryMechanisms": [ + { + "Name": "verified_phone_number", + "Priority": 1 + }, + { + "Name": "verified_email", + "Priority": 2 + } + ] + }, + "AdminCreateUserConfig": { + "AllowAdminCreateUserOnly": true + }, + "EmailVerificationMessage": "The verification code to your new account is {####}", + "EmailVerificationSubject": "Verify your new account", + "SmsVerificationMessage": "The verification code to your new account is {####}", + "UserPoolAddOns": { + "AdvancedSecurityMode": "ENFORCED" + }, + "VerificationMessageTemplate": { + "DefaultEmailOption": "CONFIRM_WITH_CODE", + "EmailMessage": "The verification code to your new account is {####}", + "EmailSubject": "Verify your new account", + "SmsMessage": "The verification code to your new account is {####}" + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "testlambdaopensearchCognitoUserPoolClient39C21D94": { + "Type": "AWS::Cognito::UserPoolClient", + "Properties": { + "UserPoolId": { + "Ref": "testlambdaopensearchCognitoUserPoolA09096F9" + }, + "AllowedOAuthFlows": [ + "implicit", + "code" + ], + "AllowedOAuthFlowsUserPoolClient": true, + "AllowedOAuthScopes": [ + "profile", + "phone", + "email", + "openid", + "aws.cognito.signin.user.admin" + ], + "CallbackURLs": [ + "https://example.com" + ], + "SupportedIdentityProviders": [ + "COGNITO" + ] + } + }, + "testlambdaopensearchCognitoIdentityPool0B1FB311": { + "Type": "AWS::Cognito::IdentityPool", + "Properties": { + "AllowUnauthenticatedIdentities": false, + "CognitoIdentityProviders": [ + { + "ClientId": { + "Ref": "testlambdaopensearchCognitoUserPoolClient39C21D94" + }, + "ProviderName": { + "Fn::GetAtt": [ + "testlambdaopensearchCognitoUserPoolA09096F9", + "ProviderName" + ] + }, + "ServerSideTokenCheck": true + } + ] + } + }, + "testlambdaopensearchUserPoolDomain98864920": { + "Type": "AWS::Cognito::UserPoolDomain", + "Properties": { + "Domain": "solutions-constructs-domain", + "UserPoolId": { + "Ref": "testlambdaopensearchCognitoUserPoolA09096F9" + } + }, + "DependsOn": [ + "testlambdaopensearchCognitoUserPoolA09096F9" + ] + }, + "testlambdaopensearchCognitoAuthorizedRole58A1ED44": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRoleWithWebIdentity", + "Condition": { + "StringEquals": { + "cognito-identity.amazonaws.com:aud": { + "Ref": "testlambdaopensearchCognitoIdentityPool0B1FB311" + } + }, + "ForAnyValue:StringLike": { + "cognito-identity.amazonaws.com:amr": "authenticated" + } + }, + "Effect": "Allow", + "Principal": { + "Federated": "cognito-identity.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": "es:ESHttp*", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":es:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":domain/solutions-constructs-domain/*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "CognitoAccessPolicy" + } + ] + } + }, + "testlambdaopensearchIdentityPoolRoleMappingD8C765B1": { + "Type": "AWS::Cognito::IdentityPoolRoleAttachment", + "Properties": { + "IdentityPoolId": { + "Ref": "testlambdaopensearchCognitoIdentityPool0B1FB311" + }, + "Roles": { + "authenticated": { + "Fn::GetAtt": [ + "testlambdaopensearchCognitoAuthorizedRole58A1ED44", + "Arn" + ] + } + } + } + }, + "testlambdaopensearchCognitoKibanaConfigureRole9623271F": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "es.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "testlambdaopensearchCognitoKibanaConfigureRolePolicyE9D9C211": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "cognito-idp:DescribeUserPool", + "cognito-idp:CreateUserPoolClient", + "cognito-idp:DeleteUserPoolClient", + "cognito-idp:DescribeUserPoolClient", + "cognito-idp:AdminInitiateAuth", + "cognito-idp:AdminUserGlobalSignOut", + "cognito-idp:ListUserPoolClients", + "cognito-identity:DescribeIdentityPool", + "cognito-identity:UpdateIdentityPool", + "cognito-identity:SetIdentityPoolRoles", + "cognito-identity:GetIdentityPoolRoles", + "es:UpdateDomainConfig" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "testlambdaopensearchCognitoUserPoolA09096F9", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:aws:cognito-identity:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":identitypool/", + { + "Ref": "testlambdaopensearchCognitoIdentityPool0B1FB311" + } + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:aws:es:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":domain/solutions-constructs-domain" + ] + ] + } + ] + }, + { + "Action": "iam:PassRole", + "Condition": { + "StringLike": { + "iam:PassedToService": "cognito-identity.amazonaws.com" + } + }, + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "testlambdaopensearchCognitoKibanaConfigureRole9623271F", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "testlambdaopensearchCognitoKibanaConfigureRolePolicyE9D9C211", + "Roles": [ + { + "Ref": "testlambdaopensearchCognitoKibanaConfigureRole9623271F" + } + ] + } + }, + "testlambdaopensearchOpenSearchDomainF9CCC3D3": { + "Type": "AWS::OpenSearchService::Domain", + "Properties": { + "AccessPolicies": { + "Statement": [ + { + "Action": "es:ESHttp*", + "Effect": "Allow", + "Principal": { + "AWS": [ + { + "Fn::GetAtt": [ + "testlambdaopensearchCognitoAuthorizedRole58A1ED44", + "Arn" + ] + }, + { + "Fn::GetAtt": [ + "testlambdaopensearchLambdaFunctionServiceRole4722AB8A", + "Arn" + ] + } + ] + }, + "Resource": { + "Fn::Join": [ + "", + [ + "arn:aws:es:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":domain/solutions-constructs-domain/*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "ClusterConfig": { + "DedicatedMasterCount": 3, + "DedicatedMasterEnabled": true, + "InstanceCount": 3, + "ZoneAwarenessConfig": { + "AvailabilityZoneCount": 3 + }, + "ZoneAwarenessEnabled": true + }, + "CognitoOptions": { + "Enabled": true, + "IdentityPoolId": { + "Ref": "testlambdaopensearchCognitoIdentityPool0B1FB311" + }, + "RoleArn": { + "Fn::GetAtt": [ + "testlambdaopensearchCognitoKibanaConfigureRole9623271F", + "Arn" + ] + }, + "UserPoolId": { + "Ref": "testlambdaopensearchCognitoUserPoolA09096F9" + } + }, + "DomainName": "solutions-constructs-domain", + "EBSOptions": { + "EBSEnabled": true, + "VolumeSize": 10 + }, + "EncryptionAtRestOptions": { + "Enabled": true + }, + "EngineVersion": "OpenSearch_1.3", + "NodeToNodeEncryptionOptions": { + "Enabled": true + }, + "SnapshotOptions": { + "AutomatedSnapshotStartHour": 1 + } + }, + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W28", + "reason": "The OpenSearch Service domain is passed dynamically as as parameter and explicitly specified to ensure that IAM policies are configured to lockdown access to this specific OpenSearch Service instance only" + }, + { + "id": "W90", + "reason": "This is not a rule for the general case, just for specific use cases/industries" + } + ] + } + } + }, + "testlambdaopensearchStatusRedAlarm1627144D": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "AlarmDescription": "At least one primary shard and its replicas are not allocated to a node. ", + "MetricName": "ClusterStatus.red", + "Namespace": "AWS/ES", + "Period": 60, + "Statistic": "Maximum", + "Threshold": 1 + } + }, + "testlambdaopensearchStatusYellowAlarm57139CF0": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "AlarmDescription": "At least one replica shard is not allocated to a node.", + "MetricName": "ClusterStatus.yellow", + "Namespace": "AWS/ES", + "Period": 60, + "Statistic": "Maximum", + "Threshold": 1 + } + }, + "testlambdaopensearchFreeStorageSpaceTooLowAlarm6A5E1E96": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "LessThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "AlarmDescription": "A node in your cluster is down to 20 GiB of free storage space.", + "MetricName": "FreeStorageSpace", + "Namespace": "AWS/ES", + "Period": 60, + "Statistic": "Minimum", + "Threshold": 20000 + } + }, + "testlambdaopensearchIndexWritesBlockedTooHighAlarmD2E041A3": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "AlarmDescription": "Your cluster is blocking write requests.", + "MetricName": "ClusterIndexWritesBlocked", + "Namespace": "AWS/ES", + "Period": 300, + "Statistic": "Maximum", + "Threshold": 1 + } + }, + "testlambdaopensearchAutomatedSnapshotFailureTooHighAlarm9A4D0B1F": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "AlarmDescription": "An automated snapshot failed. This failure is often the result of a red cluster health status.", + "MetricName": "AutomatedSnapshotFailure", + "Namespace": "AWS/ES", + "Period": 60, + "Statistic": "Maximum", + "Threshold": 1 + } + }, + "testlambdaopensearchCPUUtilizationTooHighAlarmC4850758": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 3, + "AlarmDescription": "100% CPU utilization is not uncommon, but sustained high usage is problematic. Consider using larger instance types or adding instances.", + "MetricName": "CPUUtilization", + "Namespace": "AWS/ES", + "Period": 900, + "Statistic": "Average", + "Threshold": 80 + } + }, + "testlambdaopensearchJVMMemoryPressureTooHighAlarmEFB09A7C": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "AlarmDescription": "Average JVM memory pressure over last 15 minutes too high. Consider scaling vertically.", + "MetricName": "JVMMemoryPressure", + "Namespace": "AWS/ES", + "Period": 900, + "Statistic": "Average", + "Threshold": 80 + } + }, + "testlambdaopensearchMasterCPUUtilizationTooHighAlarm124D5748": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 3, + "AlarmDescription": "Average CPU utilization over last 45 minutes too high. Consider using larger instance types for your dedicated master nodes.", + "MetricName": "MasterCPUUtilization", + "Namespace": "AWS/ES", + "Period": 900, + "Statistic": "Average", + "Threshold": 50 + } + }, + "testlambdaopensearchMasterJVMMemoryPressureTooHighAlarmBC9524D3": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "AlarmDescription": "Average JVM memory pressure over last 15 minutes too high. Consider scaling vertically.", + "MetricName": "MasterJVMMemoryPressure", + "Namespace": "AWS/ES", + "Period": 900, + "Statistic": "Average", + "Threshold": 50 + } + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/test/integ.no-arguments.ts b/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/test/integ.no-arguments.ts new file mode 100644 index 000000000..65080777a --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/test/integ.no-arguments.ts @@ -0,0 +1,38 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +/// !cdk-integ * +import { App, Stack } from "aws-cdk-lib"; +import { LambdaToOpenSearch } from "../lib"; +import * as lambda from 'aws-cdk-lib/aws-lambda'; +import { generateIntegStackName } from '@aws-solutions-constructs/core'; + +// Setup +const app = new App(); +const stack = new Stack(app, generateIntegStackName(__filename)); + +const lambdaProps: lambda.FunctionProps = { + code: lambda.Code.fromAsset(`${__dirname}/lambda`), + runtime: lambda.Runtime.NODEJS_14_X, + handler: 'index.handler' +}; + +const openSearchDomain = 'solutions-constructs-domain'; + +new LambdaToOpenSearch(stack, 'test-lambda-opensearch', { + lambdaFunctionProps: lambdaProps, + openSearchDomainName: openSearchDomain, +}); + +// Synth +app.synth(); diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/test/integ.vpc-props.expected.json b/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/test/integ.vpc-props.expected.json new file mode 100644 index 000000000..c459a8eee --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/test/integ.vpc-props.expected.json @@ -0,0 +1,1037 @@ +{ + "Resources": { + "testlambdaopensearchLambdaFunctionServiceRole4722AB8A": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:/aws/lambda/*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "LambdaFunctionServiceRolePolicy" + } + ] + } + }, + "testlambdaopensearchLambdaFunctionServiceRoleDefaultPolicy78C56359": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "ec2:CreateNetworkInterface", + "ec2:DescribeNetworkInterfaces", + "ec2:DeleteNetworkInterface", + "ec2:AssignPrivateIpAddresses", + "ec2:UnassignPrivateIpAddresses" + ], + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "xray:PutTraceSegments", + "xray:PutTelemetryRecords" + ], + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "testlambdaopensearchLambdaFunctionServiceRoleDefaultPolicy78C56359", + "Roles": [ + { + "Ref": "testlambdaopensearchLambdaFunctionServiceRole4722AB8A" + } + ] + }, + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W12", + "reason": "Lambda needs the following minimum required permissions to send trace data to X-Ray and access ENIs in a VPC." + } + ] + } + } + }, + "testlambdaopensearchReplaceDefaultSecurityGroupsecuritygroupB44718EC": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "vpc-props/test-lambda-opensearch/ReplaceDefaultSecurityGroup-security-group", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "VpcId": { + "Ref": "Vpc8378EB38" + } + }, + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W5", + "reason": "Egress of 0.0.0.0/0 is default and generally considered OK" + }, + { + "id": "W40", + "reason": "Egress IPProtocol of -1 is default and generally considered OK" + } + ] + } + } + }, + "testlambdaopensearchLambdaFunction93FD38F7": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-12345678-test-region", + "S3Key": "466cbfc6653782bd2707dec5bc5c58005c75a8bff3660a58eefa57d667120240.zip" + }, + "Role": { + "Fn::GetAtt": [ + "testlambdaopensearchLambdaFunctionServiceRole4722AB8A", + "Arn" + ] + }, + "Environment": { + "Variables": { + "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1", + "DOMAIN_ENDPOINT": { + "Fn::GetAtt": [ + "testlambdaopensearchOpenSearchDomainF9CCC3D3", + "DomainEndpoint" + ] + } + } + }, + "Handler": "index.handler", + "Runtime": "nodejs14.x", + "TracingConfig": { + "Mode": "Active" + }, + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::GetAtt": [ + "testlambdaopensearchReplaceDefaultSecurityGroupsecuritygroupB44718EC", + "GroupId" + ] + } + ], + "SubnetIds": [ + { + "Ref": "VpcisolatedSubnet1SubnetE62B1B9B" + }, + { + "Ref": "VpcisolatedSubnet2Subnet39217055" + }, + { + "Ref": "VpcisolatedSubnet3Subnet44F2537D" + } + ] + } + }, + "DependsOn": [ + "testlambdaopensearchLambdaFunctionServiceRoleDefaultPolicy78C56359", + "testlambdaopensearchLambdaFunctionServiceRole4722AB8A", + "VpcisolatedSubnet1RouteTableAssociationD259E31A", + "VpcisolatedSubnet2RouteTableAssociation25A4716F", + "VpcisolatedSubnet3RouteTableAssociationDC010BEB" + ], + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W58", + "reason": "Lambda functions has the required permission to write CloudWatch Logs. It uses custom policy instead of arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole with tighter permissions." + }, + { + "id": "W89", + "reason": "This is not a rule for the general case, just for specific use cases/industries" + }, + { + "id": "W92", + "reason": "Impossible for us to define the correct concurrency for clients" + } + ] + } + } + }, + "testlambdaopensearchCognitoUserPoolA09096F9": { + "Type": "AWS::Cognito::UserPool", + "Properties": { + "AccountRecoverySetting": { + "RecoveryMechanisms": [ + { + "Name": "verified_phone_number", + "Priority": 1 + }, + { + "Name": "verified_email", + "Priority": 2 + } + ] + }, + "AdminCreateUserConfig": { + "AllowAdminCreateUserOnly": true + }, + "EmailVerificationMessage": "The verification code to your new account is {####}", + "EmailVerificationSubject": "Verify your new account", + "SmsVerificationMessage": "The verification code to your new account is {####}", + "UserPoolAddOns": { + "AdvancedSecurityMode": "ENFORCED" + }, + "VerificationMessageTemplate": { + "DefaultEmailOption": "CONFIRM_WITH_CODE", + "EmailMessage": "The verification code to your new account is {####}", + "EmailSubject": "Verify your new account", + "SmsMessage": "The verification code to your new account is {####}" + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "testlambdaopensearchCognitoUserPoolClient39C21D94": { + "Type": "AWS::Cognito::UserPoolClient", + "Properties": { + "UserPoolId": { + "Ref": "testlambdaopensearchCognitoUserPoolA09096F9" + }, + "AllowedOAuthFlows": [ + "implicit", + "code" + ], + "AllowedOAuthFlowsUserPoolClient": true, + "AllowedOAuthScopes": [ + "profile", + "phone", + "email", + "openid", + "aws.cognito.signin.user.admin" + ], + "CallbackURLs": [ + "https://example.com" + ], + "SupportedIdentityProviders": [ + "COGNITO" + ] + } + }, + "testlambdaopensearchCognitoIdentityPool0B1FB311": { + "Type": "AWS::Cognito::IdentityPool", + "Properties": { + "AllowUnauthenticatedIdentities": false, + "CognitoIdentityProviders": [ + { + "ClientId": { + "Ref": "testlambdaopensearchCognitoUserPoolClient39C21D94" + }, + "ProviderName": { + "Fn::GetAtt": [ + "testlambdaopensearchCognitoUserPoolA09096F9", + "ProviderName" + ] + }, + "ServerSideTokenCheck": true + } + ] + } + }, + "testlambdaopensearchUserPoolDomain98864920": { + "Type": "AWS::Cognito::UserPoolDomain", + "Properties": { + "Domain": "deploytestwithvpcprops", + "UserPoolId": { + "Ref": "testlambdaopensearchCognitoUserPoolA09096F9" + } + }, + "DependsOn": [ + "testlambdaopensearchCognitoUserPoolA09096F9" + ] + }, + "testlambdaopensearchCognitoAuthorizedRole58A1ED44": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRoleWithWebIdentity", + "Condition": { + "StringEquals": { + "cognito-identity.amazonaws.com:aud": { + "Ref": "testlambdaopensearchCognitoIdentityPool0B1FB311" + } + }, + "ForAnyValue:StringLike": { + "cognito-identity.amazonaws.com:amr": "authenticated" + } + }, + "Effect": "Allow", + "Principal": { + "Federated": "cognito-identity.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": "es:ESHttp*", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":es:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":domain/deploytestwithvpcprops/*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "CognitoAccessPolicy" + } + ] + } + }, + "testlambdaopensearchIdentityPoolRoleMappingD8C765B1": { + "Type": "AWS::Cognito::IdentityPoolRoleAttachment", + "Properties": { + "IdentityPoolId": { + "Ref": "testlambdaopensearchCognitoIdentityPool0B1FB311" + }, + "Roles": { + "authenticated": { + "Fn::GetAtt": [ + "testlambdaopensearchCognitoAuthorizedRole58A1ED44", + "Arn" + ] + } + } + } + }, + "testlambdaopensearchCognitoKibanaConfigureRole9623271F": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "es.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "testlambdaopensearchCognitoKibanaConfigureRolePolicyE9D9C211": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "cognito-idp:DescribeUserPool", + "cognito-idp:CreateUserPoolClient", + "cognito-idp:DeleteUserPoolClient", + "cognito-idp:DescribeUserPoolClient", + "cognito-idp:AdminInitiateAuth", + "cognito-idp:AdminUserGlobalSignOut", + "cognito-idp:ListUserPoolClients", + "cognito-identity:DescribeIdentityPool", + "cognito-identity:UpdateIdentityPool", + "cognito-identity:SetIdentityPoolRoles", + "cognito-identity:GetIdentityPoolRoles", + "es:UpdateDomainConfig" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "testlambdaopensearchCognitoUserPoolA09096F9", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:aws:cognito-identity:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":identitypool/", + { + "Ref": "testlambdaopensearchCognitoIdentityPool0B1FB311" + } + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:aws:es:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":domain/deploytestwithvpcprops" + ] + ] + } + ] + }, + { + "Action": "iam:PassRole", + "Condition": { + "StringLike": { + "iam:PassedToService": "cognito-identity.amazonaws.com" + } + }, + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "testlambdaopensearchCognitoKibanaConfigureRole9623271F", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "testlambdaopensearchCognitoKibanaConfigureRolePolicyE9D9C211", + "Roles": [ + { + "Ref": "testlambdaopensearchCognitoKibanaConfigureRole9623271F" + } + ] + } + }, + "testlambdaopensearchOpenSearchDomainF9CCC3D3": { + "Type": "AWS::OpenSearchService::Domain", + "Properties": { + "AccessPolicies": { + "Statement": [ + { + "Action": "es:ESHttp*", + "Effect": "Allow", + "Principal": { + "AWS": [ + { + "Fn::GetAtt": [ + "testlambdaopensearchCognitoAuthorizedRole58A1ED44", + "Arn" + ] + }, + { + "Fn::GetAtt": [ + "testlambdaopensearchLambdaFunctionServiceRole4722AB8A", + "Arn" + ] + } + ] + }, + "Resource": { + "Fn::Join": [ + "", + [ + "arn:aws:es:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":domain/deploytestwithvpcprops/*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "ClusterConfig": { + "DedicatedMasterCount": 3, + "DedicatedMasterEnabled": true, + "InstanceCount": 3, + "ZoneAwarenessConfig": { + "AvailabilityZoneCount": 3 + }, + "ZoneAwarenessEnabled": true + }, + "CognitoOptions": { + "Enabled": true, + "IdentityPoolId": { + "Ref": "testlambdaopensearchCognitoIdentityPool0B1FB311" + }, + "RoleArn": { + "Fn::GetAtt": [ + "testlambdaopensearchCognitoKibanaConfigureRole9623271F", + "Arn" + ] + }, + "UserPoolId": { + "Ref": "testlambdaopensearchCognitoUserPoolA09096F9" + } + }, + "DomainName": "deploytestwithvpcprops", + "EBSOptions": { + "EBSEnabled": true, + "VolumeSize": 10 + }, + "EncryptionAtRestOptions": { + "Enabled": true + }, + "EngineVersion": "OpenSearch_1.3", + "NodeToNodeEncryptionOptions": { + "Enabled": true + }, + "SnapshotOptions": { + "AutomatedSnapshotStartHour": 1 + }, + "VPCOptions": { + "SecurityGroupIds": [ + { + "Fn::GetAtt": [ + "testlambdaopensearchReplaceDefaultSecurityGroupsecuritygroupB44718EC", + "GroupId" + ] + } + ], + "SubnetIds": [ + { + "Ref": "VpcisolatedSubnet1SubnetE62B1B9B" + }, + { + "Ref": "VpcisolatedSubnet2Subnet39217055" + }, + { + "Ref": "VpcisolatedSubnet3Subnet44F2537D" + } + ] + } + }, + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W28", + "reason": "The OpenSearch Service domain is passed dynamically as as parameter and explicitly specified to ensure that IAM policies are configured to lockdown access to this specific OpenSearch Service instance only" + }, + { + "id": "W90", + "reason": "This is not a rule for the general case, just for specific use cases/industries" + } + ] + } + } + }, + "testlambdaopensearchStatusRedAlarm1627144D": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "AlarmDescription": "At least one primary shard and its replicas are not allocated to a node. ", + "MetricName": "ClusterStatus.red", + "Namespace": "AWS/ES", + "Period": 60, + "Statistic": "Maximum", + "Threshold": 1 + } + }, + "testlambdaopensearchStatusYellowAlarm57139CF0": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "AlarmDescription": "At least one replica shard is not allocated to a node.", + "MetricName": "ClusterStatus.yellow", + "Namespace": "AWS/ES", + "Period": 60, + "Statistic": "Maximum", + "Threshold": 1 + } + }, + "testlambdaopensearchFreeStorageSpaceTooLowAlarm6A5E1E96": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "LessThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "AlarmDescription": "A node in your cluster is down to 20 GiB of free storage space.", + "MetricName": "FreeStorageSpace", + "Namespace": "AWS/ES", + "Period": 60, + "Statistic": "Minimum", + "Threshold": 20000 + } + }, + "testlambdaopensearchIndexWritesBlockedTooHighAlarmD2E041A3": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "AlarmDescription": "Your cluster is blocking write requests.", + "MetricName": "ClusterIndexWritesBlocked", + "Namespace": "AWS/ES", + "Period": 300, + "Statistic": "Maximum", + "Threshold": 1 + } + }, + "testlambdaopensearchAutomatedSnapshotFailureTooHighAlarm9A4D0B1F": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "AlarmDescription": "An automated snapshot failed. This failure is often the result of a red cluster health status.", + "MetricName": "AutomatedSnapshotFailure", + "Namespace": "AWS/ES", + "Period": 60, + "Statistic": "Maximum", + "Threshold": 1 + } + }, + "testlambdaopensearchCPUUtilizationTooHighAlarmC4850758": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 3, + "AlarmDescription": "100% CPU utilization is not uncommon, but sustained high usage is problematic. Consider using larger instance types or adding instances.", + "MetricName": "CPUUtilization", + "Namespace": "AWS/ES", + "Period": 900, + "Statistic": "Average", + "Threshold": 80 + } + }, + "testlambdaopensearchJVMMemoryPressureTooHighAlarmEFB09A7C": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "AlarmDescription": "Average JVM memory pressure over last 15 minutes too high. Consider scaling vertically.", + "MetricName": "JVMMemoryPressure", + "Namespace": "AWS/ES", + "Period": 900, + "Statistic": "Average", + "Threshold": 80 + } + }, + "testlambdaopensearchMasterCPUUtilizationTooHighAlarm124D5748": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 3, + "AlarmDescription": "Average CPU utilization over last 45 minutes too high. Consider using larger instance types for your dedicated master nodes.", + "MetricName": "MasterCPUUtilization", + "Namespace": "AWS/ES", + "Period": 900, + "Statistic": "Average", + "Threshold": 50 + } + }, + "testlambdaopensearchMasterJVMMemoryPressureTooHighAlarmBC9524D3": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "AlarmDescription": "Average JVM memory pressure over last 15 minutes too high. Consider scaling vertically.", + "MetricName": "MasterJVMMemoryPressure", + "Namespace": "AWS/ES", + "Period": 900, + "Statistic": "Average", + "Threshold": 50 + } + }, + "Vpc8378EB38": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "172.168.0.0/16", + "EnableDnsHostnames": true, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": [ + { + "Key": "Name", + "Value": "vpc-props/Vpc" + } + ] + } + }, + "VpcisolatedSubnet1SubnetE62B1B9B": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1a", + "CidrBlock": "172.168.0.0/18", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "isolated" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Isolated" + }, + { + "Key": "Name", + "Value": "vpc-props/Vpc/isolatedSubnet1" + } + ] + } + }, + "VpcisolatedSubnet1RouteTableE442650B": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "vpc-props/Vpc/isolatedSubnet1" + } + ] + } + }, + "VpcisolatedSubnet1RouteTableAssociationD259E31A": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcisolatedSubnet1RouteTableE442650B" + }, + "SubnetId": { + "Ref": "VpcisolatedSubnet1SubnetE62B1B9B" + } + } + }, + "VpcisolatedSubnet2Subnet39217055": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1b", + "CidrBlock": "172.168.64.0/18", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "isolated" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Isolated" + }, + { + "Key": "Name", + "Value": "vpc-props/Vpc/isolatedSubnet2" + } + ] + } + }, + "VpcisolatedSubnet2RouteTable334F9764": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "vpc-props/Vpc/isolatedSubnet2" + } + ] + } + }, + "VpcisolatedSubnet2RouteTableAssociation25A4716F": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcisolatedSubnet2RouteTable334F9764" + }, + "SubnetId": { + "Ref": "VpcisolatedSubnet2Subnet39217055" + } + } + }, + "VpcisolatedSubnet3Subnet44F2537D": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1c", + "CidrBlock": "172.168.128.0/18", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "isolated" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Isolated" + }, + { + "Key": "Name", + "Value": "vpc-props/Vpc/isolatedSubnet3" + } + ] + } + }, + "VpcisolatedSubnet3RouteTableA2F6BBC0": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "vpc-props/Vpc/isolatedSubnet3" + } + ] + } + }, + "VpcisolatedSubnet3RouteTableAssociationDC010BEB": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcisolatedSubnet3RouteTableA2F6BBC0" + }, + "SubnetId": { + "Ref": "VpcisolatedSubnet3Subnet44F2537D" + } + } + }, + "VpcFlowLogIAMRole6A475D41": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "vpc-flow-logs.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Tags": [ + { + "Key": "Name", + "Value": "vpc-props/Vpc" + } + ] + } + }, + "VpcFlowLogIAMRoleDefaultPolicy406FB995": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogStream", + "logs:PutLogEvents", + "logs:DescribeLogStreams" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "VpcFlowLogLogGroup7B5C56B9", + "Arn" + ] + } + }, + { + "Action": "iam:PassRole", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "VpcFlowLogIAMRole6A475D41", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "VpcFlowLogIAMRoleDefaultPolicy406FB995", + "Roles": [ + { + "Ref": "VpcFlowLogIAMRole6A475D41" + } + ] + } + }, + "VpcFlowLogLogGroup7B5C56B9": { + "Type": "AWS::Logs::LogGroup", + "Properties": { + "RetentionInDays": 731, + "Tags": [ + { + "Key": "Name", + "Value": "vpc-props/Vpc" + } + ] + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain", + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W84", + "reason": "By default CloudWatchLogs LogGroups data is encrypted using the CloudWatch server-side encryption keys (AWS Managed Keys)" + } + ] + } + } + }, + "VpcFlowLog8FF33A73": { + "Type": "AWS::EC2::FlowLog", + "Properties": { + "ResourceId": { + "Ref": "Vpc8378EB38" + }, + "ResourceType": "VPC", + "TrafficType": "ALL", + "DeliverLogsPermissionArn": { + "Fn::GetAtt": [ + "VpcFlowLogIAMRole6A475D41", + "Arn" + ] + }, + "LogDestinationType": "cloud-watch-logs", + "LogGroupName": { + "Ref": "VpcFlowLogLogGroup7B5C56B9" + }, + "Tags": [ + { + "Key": "Name", + "Value": "vpc-props/Vpc" + } + ] + } + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/test/integ.vpc-props.ts b/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/test/integ.vpc-props.ts new file mode 100644 index 000000000..f7ebd70c7 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/test/integ.vpc-props.ts @@ -0,0 +1,44 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { App, Stack } from "aws-cdk-lib"; +import { LambdaToOpenSearch } from "../lib"; +import * as lambda from 'aws-cdk-lib/aws-lambda'; +import * as defaults from '@aws-solutions-constructs/core'; + +// Setup +const app = new App(); +const stack = new Stack(app, defaults.generateIntegStackName(__filename), { + env: { + region: process.env.CDK_DEFAULT_REGION, + account: process.env.CDK_DEFAULT_ACCOUNT, + } +}); + +const lambdaProps: lambda.FunctionProps = { + code: lambda.Code.fromAsset(`${__dirname}/lambda`), + runtime: lambda.Runtime.NODEJS_14_X, + handler: 'index.handler', +}; + +new LambdaToOpenSearch(stack, 'test-lambda-opensearch', { + lambdaFunctionProps: lambdaProps, + openSearchDomainName: "deploytestwithvpcprops", + deployVpc: true, + vpcProps: { + cidr: '172.168.0.0/16', + } +}); + +// Synth +app.synth(); \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/test/lambda-opensearch.test.ts b/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/test/lambda-opensearch.test.ts new file mode 100644 index 000000000..09c6b995f --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/test/lambda-opensearch.test.ts @@ -0,0 +1,583 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { LambdaToOpenSearch, LambdaToOpenSearchProps } from "../lib"; +import * as lambda from 'aws-cdk-lib/aws-lambda'; +import * as ec2 from 'aws-cdk-lib/aws-ec2'; +import * as cdk from "aws-cdk-lib"; +import '@aws-cdk/assert/jest'; +import * as defaults from '@aws-solutions-constructs/core'; + +function deployLambdaToOpenSearch(stack: cdk.Stack) { + const props: LambdaToOpenSearchProps = { + lambdaFunctionProps: getDefaultTestLambdaProps(), + openSearchDomainName: 'test-domain' + }; + + return new LambdaToOpenSearch(stack, 'test-lambda-opensearch-stack', props); +} + +function getDefaultTestLambdaProps(): lambda.FunctionProps { + return { + code: lambda.Code.fromAsset(`${__dirname}/lambda`), + runtime: lambda.Runtime.NODEJS_14_X, + handler: 'index.handler', + }; +} + +test('Check pattern properties', () => { + const stack = new cdk.Stack(); + + const construct: LambdaToOpenSearch = deployLambdaToOpenSearch(stack); + + expect(construct.lambdaFunction).toBeDefined(); + expect(construct.openSearchDomain).toBeDefined(); + expect(construct.identityPool).toBeDefined(); + expect(construct.userPool).toBeDefined(); + expect(construct.userPoolClient).toBeDefined(); + expect(construct.cloudWatchAlarms).toBeDefined(); + expect(construct.openSearchRole).toBeDefined(); +}); + +test('Check properties with no CloudWatch alarms ', () => { + const stack = new cdk.Stack(); + + const props: LambdaToOpenSearchProps = { + lambdaFunctionProps: getDefaultTestLambdaProps(), + openSearchDomainName: 'test-domain', + createCloudWatchAlarms: false + }; + + const construct = new LambdaToOpenSearch(stack, 'test-lambda-opensearch-stack', props); + + expect(construct.lambdaFunction).toBeDefined(); + expect(construct.openSearchDomain).toBeDefined(); + expect(construct.identityPool).toBeDefined(); + expect(construct.userPool).toBeDefined(); + expect(construct.userPoolClient).toBeDefined(); + expect(construct.cloudWatchAlarms).toBeUndefined(); + expect(construct.openSearchRole).toBeDefined(); +}); + +test('Check for an existing Lambda object', () => { + const stack = new cdk.Stack(); + + const existingLambdaObj = defaults.buildLambdaFunction(stack, { + lambdaFunctionProps: { + code: lambda.Code.fromAsset(`${__dirname}/lambda`), + runtime: lambda.Runtime.NODEJS_14_X, + handler: 'index.handler', + functionName: 'test-function' + } + }); + + const props: LambdaToOpenSearchProps = { + openSearchDomainName: 'test-domain', + existingLambdaObj + }; + + new LambdaToOpenSearch(stack, 'test-lambda-opensearch-stack', props); + + expect(stack).toHaveResource('AWS::Lambda::Function', { + FunctionName: 'test-function' + }); + +}); + +test('Check Lambda function custom environment variable', () => { + const stack = new cdk.Stack(); + const props: LambdaToOpenSearchProps = { + lambdaFunctionProps: getDefaultTestLambdaProps(), + openSearchDomainName: 'test-domain', + domainEndpointEnvironmentVariableName: 'CUSTOM_DOMAIN_ENDPOINT' + }; + + new LambdaToOpenSearch(stack, 'test-lambda-opensearch-stack', props); + + expect(stack).toHaveResource('AWS::Lambda::Function', { + Handler: 'index.handler', + Runtime: 'nodejs14.x', + Environment: { + Variables: { + AWS_NODEJS_CONNECTION_REUSE_ENABLED: '1', + CUSTOM_DOMAIN_ENDPOINT: { + 'Fn::GetAtt': [ + 'testlambdaopensearchstackOpenSearchDomain46D6A86E', + 'DomainEndpoint' + ] + } + } + } + }); +}); + +test('Check domain name', () => { + const stack = new cdk.Stack(); + + deployLambdaToOpenSearch(stack); + + expect(stack).toHaveResource('AWS::Cognito::UserPoolDomain', { + Domain: "test-domain", + UserPoolId: { + Ref: "testlambdaopensearchstackCognitoUserPoolF5169460" + } + }); + + expect(stack).toHaveResource('AWS::OpenSearchService::Domain', { + DomainName: "test-domain", + }); +}); + +test('Check cognito domain name override', () => { + const stack = new cdk.Stack(); + const props: LambdaToOpenSearchProps = { + lambdaFunctionProps: getDefaultTestLambdaProps(), + openSearchDomainName: 'test-domain', + cognitoDomainName: 'test-cogn-domain' + }; + + new LambdaToOpenSearch(stack, 'test-lambda-opensearch-stack', props); + + expect(stack).toHaveResource('AWS::Cognito::UserPoolDomain', { + Domain: 'test-cogn-domain' + }); +}); + +test('Check engine version override', () => { + const stack = new cdk.Stack(); + const props: LambdaToOpenSearchProps = { + lambdaFunctionProps: getDefaultTestLambdaProps(), + openSearchDomainName: 'test-domain', + openSearchDomainProps: { + engineVersion: 'OpenSearch_1.0', + } + }; + + new LambdaToOpenSearch(stack, 'test-lambda-opensearch-stack', props); + + expect(stack).toHaveResource('AWS::OpenSearchService::Domain', { + EngineVersion: 'OpenSearch_1.0' + }); +}); + +test("Test minimal deployment that deploys a VPC in 2 AZ without vpcProps", () => { + const stack = new cdk.Stack(undefined, undefined, {}); + + new LambdaToOpenSearch(stack, "lambda-opensearch-stack", { + lambdaFunctionProps: getDefaultTestLambdaProps(), + openSearchDomainName: 'test-domain', + deployVpc: true, + }); + + expect(stack).toHaveResource("AWS::Lambda::Function", { + VpcConfig: { + SecurityGroupIds: [ + { + "Fn::GetAtt": [ + "lambdaopensearchstackReplaceDefaultSecurityGroupsecuritygroup293B90D7", + "GroupId", + ], + }, + ], + SubnetIds: [ + { + Ref: "VpcisolatedSubnet1SubnetE62B1B9B", + }, + { + Ref: "VpcisolatedSubnet2Subnet39217055", + } + ], + }, + }); + + expect(stack).toHaveResourceLike("AWS::OpenSearchService::Domain", { + VPCOptions: { + SubnetIds: [ + { + Ref: "VpcisolatedSubnet1SubnetE62B1B9B" + }, + { + Ref: "VpcisolatedSubnet2Subnet39217055" + } + ] + } + }); + + expect(stack).toHaveResource("AWS::EC2::VPC", { + EnableDnsHostnames: true, + EnableDnsSupport: true, + }); +}); + +test("Test minimal deployment that deploys a VPC in 3 AZ without vpcProps", () => { + const stack = new cdk.Stack(undefined, undefined, { + env: { account: "123456789012", region: 'us-east-1' }, + }); + + new LambdaToOpenSearch(stack, "lambda-opensearch-stack", { + lambdaFunctionProps: getDefaultTestLambdaProps(), + openSearchDomainName: 'test-domain', + deployVpc: true, + }); + + expect(stack).toHaveResource("AWS::Lambda::Function", { + VpcConfig: { + SecurityGroupIds: [ + { + "Fn::GetAtt": [ + "lambdaopensearchstackReplaceDefaultSecurityGroupsecuritygroup293B90D7", + "GroupId", + ], + }, + ], + SubnetIds: [ + { + Ref: "VpcisolatedSubnet1SubnetE62B1B9B", + }, + { + Ref: "VpcisolatedSubnet2Subnet39217055", + }, + { + Ref: "VpcisolatedSubnet3Subnet44F2537D", + }, + ], + }, + }); + + expect(stack).toHaveResourceLike("AWS::OpenSearchService::Domain", { + VPCOptions: { + SubnetIds: [ + { + Ref: "VpcisolatedSubnet1SubnetE62B1B9B" + }, + { + Ref: "VpcisolatedSubnet2Subnet39217055" + }, + { + Ref: "VpcisolatedSubnet3Subnet44F2537D" + } + ] + } + }); + + expect(stack).toHaveResource("AWS::EC2::VPC", { + EnableDnsHostnames: true, + EnableDnsSupport: true, + }); +}); + +test("Test cluster deploy to 1 AZ when user set zoneAwarenessEnabled to false", () => { + const stack = new cdk.Stack(undefined, undefined, { + env: { account: "123456789012", region: 'us-east-1' }, + }); + + const openSearchDomainProps = { + clusterConfig: { + dedicatedMasterCount: 3, + dedicatedMasterEnabled: true, + zoneAwarenessEnabled: false, + instanceCount: 3 + } + }; + + new LambdaToOpenSearch(stack, "lambda-opensearch-stack", { + lambdaFunctionProps: getDefaultTestLambdaProps(), + openSearchDomainName: 'test-domain', + openSearchDomainProps, + deployVpc: true, + vpcProps: { + maxAzs: 1 + } + }); + + expect(stack).toHaveResource("AWS::OpenSearchService::Domain", { + ClusterConfig: { + DedicatedMasterCount: 3, + DedicatedMasterEnabled: true, + InstanceCount: 3, + ZoneAwarenessEnabled: false, + } + }); + + expect(stack).toHaveResourceLike("AWS::OpenSearchService::Domain", { + VPCOptions: { + SubnetIds: [ + { + Ref: "VpcisolatedSubnet1SubnetE62B1B9B" + } + ] + } + }); +}); + +test("Test cluster deploy to 2 AZ when user set availabilityZoneCount to 2", () => { + const stack = new cdk.Stack(undefined, undefined, { + env: { account: "123456789012", region: 'us-east-1' }, + }); + + const openSearchDomainProps = { + clusterConfig: { + dedicatedMasterCount: 3, + dedicatedMasterEnabled: true, + instanceCount: 2, + zoneAwarenessEnabled: true, + zoneAwarenessConfig: { + availabilityZoneCount: 2 + } + } + }; + + new LambdaToOpenSearch(stack, "lambda-opensearch-stack", { + lambdaFunctionProps: getDefaultTestLambdaProps(), + openSearchDomainName: 'test-domain', + openSearchDomainProps, + deployVpc: true, + vpcProps: { + maxAzs: 2 + } + }); + + expect(stack).toHaveResource("AWS::OpenSearchService::Domain", { + ClusterConfig: { + DedicatedMasterCount: 3, + DedicatedMasterEnabled: true, + InstanceCount: 2, + ZoneAwarenessConfig: { + AvailabilityZoneCount: 2, + }, + ZoneAwarenessEnabled: true, + } + }); + + expect(stack).toHaveResourceLike("AWS::OpenSearchService::Domain", { + VPCOptions: { + SubnetIds: [ + { + Ref: "VpcisolatedSubnet1SubnetE62B1B9B" + }, + { + Ref: "VpcisolatedSubnet2Subnet39217055" + } + ] + } + }); +}); + +test('Test minimal deployment with an existing isolated VPC', () => { + const stack = new cdk.Stack(undefined, undefined, { + env: { account: "123456789012", region: 'us-east-1' }, + }); + + const vpc = defaults.getTestVpc(stack, false, { + vpcName: "existing-isolated-vpc-test" + }); + + const construct = new LambdaToOpenSearch(stack, 'test-lambda-opensearch', { + lambdaFunctionProps: getDefaultTestLambdaProps(), + openSearchDomainName: "test", + existingVpc: vpc + }); + + expect(stack).toHaveResourceLike("AWS::EC2::VPC", { + Tags: [ + { + Key: "Name", + Value: "existing-isolated-vpc-test" + } + ] + }); + + expect(stack).toHaveResourceLike("AWS::OpenSearchService::Domain", { + VPCOptions: { + SubnetIds: [ + { + Ref: "VpcisolatedSubnet1SubnetE62B1B9B" + }, + { + Ref: "VpcisolatedSubnet2Subnet39217055" + }, + { + Ref: "VpcisolatedSubnet3Subnet44F2537D" + } + ] + } + }); + + expect(stack).toCountResources("AWS::EC2::VPC", 1); + expect(construct.vpc).toBeDefined(); +}); + +test('Test minimal deployment with an existing private VPC', () => { + const stack = new cdk.Stack(undefined, undefined, { + env: { account: "123456789012", region: 'us-east-1' }, + }); + + const vpc = new ec2.Vpc(stack, 'existing-private-vpc-test', { + natGateways: 1, + subnetConfiguration: [ + { + cidrMask: 24, + name: 'application', + subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS, + }, + { + cidrMask: 24, + name: "public", + subnetType: ec2.SubnetType.PUBLIC, + } + ] + }); + + const construct = new LambdaToOpenSearch(stack, 'test-lambda-opensearch', { + lambdaFunctionProps: getDefaultTestLambdaProps(), + openSearchDomainName: "test", + existingVpc: vpc + }); + + expect(stack).toHaveResourceLike("AWS::EC2::VPC", { + Tags: [ + { + Key: "Name", + Value: "Default/existing-private-vpc-test" + } + ] + }); + + expect(stack).toHaveResourceLike("AWS::OpenSearchService::Domain", { + VPCOptions: { + SubnetIds: [ + { + Ref: "existingprivatevpctestapplicationSubnet1Subnet1F7744F0" + }, + { + Ref: "existingprivatevpctestapplicationSubnet2SubnetF7B713AD" + }, + { + Ref: "existingprivatevpctestapplicationSubnet3SubnetA519E038" + } + ] + } + }); + + expect(stack).toCountResources("AWS::EC2::VPC", 1); + expect(construct.vpc).toBeDefined(); +}); + +test('Test minimal deployment with VPC construct props', () => { + const stack = new cdk.Stack(undefined, undefined, { + env: { account: "123456789012", region: 'us-east-1' }, + }); + + const construct = new LambdaToOpenSearch(stack, 'test-lambda-opensearch', { + lambdaFunctionProps: getDefaultTestLambdaProps(), + openSearchDomainName: "test", + deployVpc: true, + vpcProps: { + vpcName: "vpc-props-test" + } + }); + + expect(stack).toHaveResourceLike("AWS::EC2::VPC", { + Tags: [ + { + Key: "Name", + Value: "vpc-props-test" + } + ] + }); + + expect(stack).toHaveResourceLike("AWS::OpenSearchService::Domain", { + VPCOptions: { + SubnetIds: [ + { + Ref: "VpcisolatedSubnet1SubnetE62B1B9B" + }, + { + Ref: "VpcisolatedSubnet2Subnet39217055" + }, + { + Ref: "VpcisolatedSubnet3Subnet44F2537D" + } + ] + } + }); + + expect(stack).toCountResources("AWS::EC2::VPC", 1); + expect(construct.vpc).toBeDefined(); +}); + +test('Test error for vpcProps and undefined deployVpc prop', () => { + const stack = new cdk.Stack(); + + const app = () => { + new LambdaToOpenSearch(stack, 'test-lambda-opensearch', { + lambdaFunctionProps: getDefaultTestLambdaProps(), + openSearchDomainName: "test", + vpcProps: { + vpcName: "existing-vpc-test" + } + }); + }; + + expect(app).toThrowError("Error - deployVpc must be true when defining vpcProps"); +}); + +test('Test error for Lambda function VPC props', () => { + const stack = new cdk.Stack(); + + const vpc = defaults.getTestVpc(stack); + + const app = () => { + new LambdaToOpenSearch(stack, 'test-lambda-opensearch', { + lambdaFunctionProps: defaults.consolidateProps(getDefaultTestLambdaProps(), { vpc }), + openSearchDomainName: "test", + deployVpc: true, + }); + }; + + expect(app).toThrowError("Error - Define VPC using construct parameters not Lambda function props"); +}); + +test('Test error for the OpenSearch domain VPC props', () => { + const stack = new cdk.Stack(); + + const app = () => { + new LambdaToOpenSearch(stack, 'test-lambda-opensearch', { + lambdaFunctionProps: getDefaultTestLambdaProps(), + openSearchDomainProps: { + vpcOptions: { + subnetIds: ['fake-ids'], + securityGroupIds: ['fake-sgs'] + } + }, + openSearchDomainName: "test", + deployVpc: true, + }); + }; + + expect(app).toThrowError("Error - Define VPC using construct parameters not the OpenSearch Service props"); +}); + +test('Test error for missing existingLambdaObj or lambdaFunctionProps', () => { + const stack = new cdk.Stack(); + + const props: LambdaToOpenSearchProps = { + openSearchDomainName: 'test-domain' + }; + + try { + new LambdaToOpenSearch(stack, 'test-lambda-opensearch-stack', props); + } catch (e) { + expect(e).toBeInstanceOf(Error); + } +}); diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/test/lambda/index.js b/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/test/lambda/index.js new file mode 100644 index 000000000..b94797307 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/test/lambda/index.js @@ -0,0 +1,60 @@ +var AWS = require('aws-sdk'); +var path = require('path'); + +console.log('Loading function'); + +var openSearchDomain = { + endpoint: process.env.DOMAIN_ENDPOINT, + region: process.env.AWS_REGION, + index: 'records', + doctype: 'movie' +}; + +var creds = new AWS.EnvironmentCredentials('AWS'); +var endpoint = new AWS.Endpoint(openSearchDomain.endpoint); + +function postDocumentToOpenSearch(doc, context) { + var req = new AWS.HttpRequest(endpoint); + + req.method = 'POST'; + req.path = path.join('/', openSearchDomain.index, openSearchDomain.doctype); + req.region = openSearchDomain.region; + req.body = doc; + req.headers['presigned-expires'] = false; + req.headers['Host'] = openSearchDomain.endpoint; + req.headers['Content-Type'] = 'application/json'; + + // Sign the request (Sigv4) + var signer = new AWS.Signers.V4(req, 'es'); + signer.addAuthorization(creds, new Date()); + + // Post document to the OpenSearch Service + var send = new AWS.NodeHttpClient(); + + send.handleRequest(req, null, (httpResp) => { + var body = ''; + httpResp.on('data', (chunk) => { + body += chunk; + }); + httpResp.on('end', (chunk) => { + console.log('All movie records added to the OpenSearch Service.'); + context.succeed(); + }); + }, (err) => { + console.log('Error: ' + err); + context.fail(); + }); +} + +exports.handler = (event, context) => { + console.log('Received event:', JSON.stringify(event, null, 2)); + postDocumentToOpenSearch("{ \"title\": \"Spirited Away\" }", context); + postDocumentToOpenSearch("{ \"title\": \"Fast and Furious\" }", context); + postDocumentToOpenSearch("{ \"title\": \"Jurassic Park\" }", context); + + return { + statusCode: 200, + headers: { 'Content-Type': 'text/plain' }, + body: `Hello from AWS Solutions Constructs You've hit ${event.path}\n` + }; +}; \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/core/index.ts b/source/patterns/@aws-solutions-constructs/core/index.ts index 5e1aee4e8..97a4b41b1 100644 --- a/source/patterns/@aws-solutions-constructs/core/index.ts +++ b/source/patterns/@aws-solutions-constructs/core/index.ts @@ -69,4 +69,6 @@ export * from './test/test-helper'; export * from './lib/ssm-string-parameter-helper'; export * from './lib/eventbridge-helper'; export * from './lib/waf-defaults'; -export * from './lib/waf-helper'; \ No newline at end of file +export * from './lib/waf-helper'; +export * from './lib/opensearch-defaults'; +export * from './lib/opensearch-helper'; \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/core/lib/cognito-helper.ts b/source/patterns/@aws-solutions-constructs/core/lib/cognito-helper.ts index ee3a775c3..1c7a9b62d 100644 --- a/source/patterns/@aws-solutions-constructs/core/lib/cognito-helper.ts +++ b/source/patterns/@aws-solutions-constructs/core/lib/cognito-helper.ts @@ -120,3 +120,18 @@ export function setupCognitoForElasticSearch(scope: Construct, domainName: strin return cognitoAuthorizedRole; } + +export function setupOpenSearchCognito(scope: Construct, domainName: string): + [cognito.UserPool, cognito.UserPoolClient, cognito.CfnIdentityPool, iam.Role] { + const userPool = buildUserPool(scope); + const userPoolClient = buildUserPoolClient(scope, userPool); + const identityPool = buildIdentityPool(scope, userPool, userPoolClient); + + const cognitoAuthorizedRole: iam.Role = setupCognitoForElasticSearch(scope, domainName, { + userpool: userPool, + identitypool: identityPool, + userpoolclient: userPoolClient + }); + + return [userPool, userPoolClient, identityPool, cognitoAuthorizedRole]; +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/core/lib/opensearch-defaults.ts b/source/patterns/@aws-solutions-constructs/core/lib/opensearch-defaults.ts new file mode 100644 index 000000000..b27cc79df --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/core/lib/opensearch-defaults.ts @@ -0,0 +1,65 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as opensearch from 'aws-cdk-lib/aws-opensearchservice'; +import * as iam from 'aws-cdk-lib/aws-iam'; +import * as cdk from 'aws-cdk-lib'; +import { BuildOpenSearchProps } from './opensearch-helper'; + +export function DefaultOpenSearchCfnDomainProps(domainName: string, cognitoConfigureRole: iam.Role, props: BuildOpenSearchProps): + opensearch.CfnDomainProps { + const roleARNs: iam.IPrincipal[] = []; + + roleARNs.push(new iam.ArnPrincipal(props.cognitoAuthorizedRoleARN)); + + if (props.serviceRoleARN) { + roleARNs.push(new iam.ArnPrincipal(props.serviceRoleARN)); + } + + return { + domainName, + engineVersion: 'OpenSearch_1.3', + encryptionAtRestOptions: { + enabled: true + }, + nodeToNodeEncryptionOptions: { + enabled: true + }, + snapshotOptions: { + automatedSnapshotStartHour: 1 + }, + ebsOptions: { + ebsEnabled: true, + volumeSize: 10 + }, + cognitoOptions: { + enabled: true, + identityPoolId: props.identitypool.ref, + userPoolId: props.userpool.userPoolId, + roleArn: cognitoConfigureRole.roleArn + }, + accessPolicies: new iam.PolicyDocument({ + statements: [ + new iam.PolicyStatement({ + principals: roleARNs, + actions: [ + 'es:ESHttp*' + ], + resources: [ + `arn:aws:es:${cdk.Aws.REGION}:${cdk.Aws.ACCOUNT_ID}:domain/${domainName}/*` + ] + }) + ] + }) + } as opensearch.CfnDomainProps; +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/core/lib/opensearch-helper.ts b/source/patterns/@aws-solutions-constructs/core/lib/opensearch-helper.ts new file mode 100644 index 000000000..64c5ee8d7 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/core/lib/opensearch-helper.ts @@ -0,0 +1,306 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as opensearch from 'aws-cdk-lib/aws-opensearchservice'; +import { DefaultOpenSearchCfnDomainProps } from './opensearch-defaults'; +import { consolidateProps, addCfnSuppressRules } from './utils'; +import * as iam from 'aws-cdk-lib/aws-iam'; +import * as cdk from 'aws-cdk-lib'; +import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch'; +import * as cognito from 'aws-cdk-lib/aws-cognito'; +import * as ec2 from 'aws-cdk-lib/aws-ec2'; +// Note: To ensure CDKv2 compatibility, keep the import statement for Construct separate +import { Construct } from 'constructs'; + +const MAXIMUM_AZS_IN_OPENSEARCH_DOMAIN = 3; + +export interface BuildOpenSearchProps { + readonly identitypool: cognito.CfnIdentityPool; + readonly userpool: cognito.UserPool; + readonly cognitoAuthorizedRoleARN: string; + readonly serviceRoleARN?: string; + readonly vpc?: ec2.IVpc; + readonly openSearchDomainName: string; + readonly clientDomainProps?: opensearch.CfnDomainProps, + readonly securityGroupIds?: string[] +} + +export function buildOpenSearch(scope: Construct, props: BuildOpenSearchProps): [opensearch.CfnDomain, iam.Role] { + let subnetIds: string[] = []; + const constructDrivenProps: any = {}; + + // Setup the IAM Role & policy for the OpenSearch Service to configure Cognito User pool and Identity pool + const cognitoKibanaConfigureRole = createKibanaCognitoRole(scope, props.userpool, props.identitypool, props.openSearchDomainName); + + if (props.vpc) { + subnetIds = retrievePrivateSubnetIds(props.vpc); + + if (subnetIds.length > MAXIMUM_AZS_IN_OPENSEARCH_DOMAIN) { + subnetIds = subnetIds.slice(0, MAXIMUM_AZS_IN_OPENSEARCH_DOMAIN); + } + + constructDrivenProps.vpcOptions = { + subnetIds, + securityGroupIds: props.securityGroupIds + }; + + // If the client did not submit a ClusterConfig, then we will create one + if (!props.clientDomainProps?.clusterConfig) { + constructDrivenProps.clusterConfig = createClusterConfiguration(subnetIds.length); + } + } else { // No VPC + // If the client did not submit a ClusterConfig, then we will create one based on the Region + if (!props.clientDomainProps?.clusterConfig) { + constructDrivenProps.clusterConfig = createClusterConfiguration(cdk.Stack.of(scope).availabilityZones.length); + } + } + + const defaultCfnDomainProps = DefaultOpenSearchCfnDomainProps(props.openSearchDomainName, cognitoKibanaConfigureRole, props); + const finalCfnDomainProps = consolidateProps(defaultCfnDomainProps, props.clientDomainProps, constructDrivenProps); + + const opensearchDomain = new opensearch.CfnDomain(scope, `OpenSearchDomain`, finalCfnDomainProps); + + addCfnSuppressRules(opensearchDomain, [ + { + id: "W28", + reason: `The OpenSearch Service domain is passed dynamically as as parameter and explicitly specified to ensure that IAM policies are configured to lockdown access to this specific OpenSearch Service instance only`, + }, + { + id: "W90", + reason: `This is not a rule for the general case, just for specific use cases/industries`, + }, + ]); + + return [opensearchDomain, cognitoKibanaConfigureRole]; +} + +export function buildOpenSearchCWAlarms(scope: Construct): cloudwatch.Alarm[] { + const alarms: cloudwatch.Alarm[] = new Array(); + + // ClusterStatus.red maximum is >= 1 for 1 minute, 1 consecutive time + alarms.push(new cloudwatch.Alarm(scope, 'StatusRedAlarm', { + metric: new cloudwatch.Metric({ + namespace: 'AWS/ES', + metricName: 'ClusterStatus.red', + statistic: 'Maximum', + period: cdk.Duration.seconds(60), + }), + threshold: 1, + evaluationPeriods: 1, + comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD, + alarmDescription: 'At least one primary shard and its replicas are not allocated to a node. ' + })); + + // ClusterStatus.yellow maximum is >= 1 for 1 minute, 1 consecutive time + alarms.push(new cloudwatch.Alarm(scope, 'StatusYellowAlarm', { + metric: new cloudwatch.Metric({ + namespace: 'AWS/ES', + metricName: 'ClusterStatus.yellow', + statistic: 'Maximum', + period: cdk.Duration.seconds(60), + }), + threshold: 1, + evaluationPeriods: 1, + comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD, + alarmDescription: 'At least one replica shard is not allocated to a node.' + })); + + // FreeStorageSpace minimum is <= 20480 for 1 minute, 1 consecutive time + alarms.push(new cloudwatch.Alarm(scope, 'FreeStorageSpaceTooLowAlarm', { + metric: new cloudwatch.Metric({ + namespace: 'AWS/ES', + metricName: 'FreeStorageSpace', + statistic: 'Minimum', + period: cdk.Duration.seconds(60), + }), + threshold: 20000, + evaluationPeriods: 1, + comparisonOperator: cloudwatch.ComparisonOperator.LESS_THAN_OR_EQUAL_TO_THRESHOLD, + alarmDescription: 'A node in your cluster is down to 20 GiB of free storage space.' + })); + + // ClusterIndexWritesBlocked is >= 1 for 5 minutes, 1 consecutive time + alarms.push(new cloudwatch.Alarm(scope, 'IndexWritesBlockedTooHighAlarm', { + metric: new cloudwatch.Metric({ + namespace: 'AWS/ES', + metricName: 'ClusterIndexWritesBlocked', + statistic: 'Maximum', + period: cdk.Duration.seconds(300), + }), + threshold: 1, + evaluationPeriods: 1, + comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD, + alarmDescription: 'Your cluster is blocking write requests.' + })); + + // AutomatedSnapshotFailure maximum is >= 1 for 1 minute, 1 consecutive time + alarms.push(new cloudwatch.Alarm(scope, 'AutomatedSnapshotFailureTooHighAlarm', { + metric: new cloudwatch.Metric({ + namespace: 'AWS/ES', + metricName: 'AutomatedSnapshotFailure', + statistic: 'Maximum', + period: cdk.Duration.seconds(60), + }), + threshold: 1, + evaluationPeriods: 1, + comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD, + alarmDescription: 'An automated snapshot failed. This failure is often the result of a red cluster health status.' + })); + + // CPUUtilization maximum is >= 80% for 15 minutes, 3 consecutive times + alarms.push(new cloudwatch.Alarm(scope, 'CPUUtilizationTooHighAlarm', { + metric: new cloudwatch.Metric({ + namespace: 'AWS/ES', + metricName: 'CPUUtilization', + statistic: 'Average', + period: cdk.Duration.seconds(900), + }), + threshold: 80, + evaluationPeriods: 3, + comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD, + alarmDescription: '100% CPU utilization is not uncommon, but sustained high usage is problematic. Consider using larger instance types or adding instances.' + })); + + // JVMMemoryPressure maximum is >= 80% for 5 minutes, 3 consecutive times + alarms.push(new cloudwatch.Alarm(scope, 'JVMMemoryPressureTooHighAlarm', { + metric: new cloudwatch.Metric({ + namespace: 'AWS/ES', + metricName: 'JVMMemoryPressure', + statistic: 'Average', + period: cdk.Duration.seconds(900), + }), + threshold: 80, + evaluationPeriods: 1, + comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD, + alarmDescription: 'Average JVM memory pressure over last 15 minutes too high. Consider scaling vertically.' + })); + + // MasterCPUUtilization maximum is >= 50% for 15 minutes, 3 consecutive times + alarms.push(new cloudwatch.Alarm(scope, 'MasterCPUUtilizationTooHighAlarm', { + metric: new cloudwatch.Metric({ + namespace: 'AWS/ES', + metricName: 'MasterCPUUtilization', + statistic: 'Average', + period: cdk.Duration.seconds(900), + }), + threshold: 50, + evaluationPeriods: 3, + comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD, + alarmDescription: 'Average CPU utilization over last 45 minutes too high. Consider using larger instance types for your dedicated master nodes.' + })); + + // MasterJVMMemoryPressure maximum is >= 80% for 15 minutes, 1 consecutive time + alarms.push(new cloudwatch.Alarm(scope, 'MasterJVMMemoryPressureTooHighAlarm', { + metric: new cloudwatch.Metric({ + namespace: 'AWS/ES', + metricName: 'MasterJVMMemoryPressure', + statistic: 'Average', + period: cdk.Duration.seconds(900), + }), + threshold: 50, + evaluationPeriods: 1, + comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD, + alarmDescription: 'Average JVM memory pressure over last 15 minutes too high. Consider scaling vertically.' + })); + + return alarms; +} + +function retrievePrivateSubnetIds(vpc: ec2.IVpc) { + let targetSubnetType; + + if (vpc.isolatedSubnets.length) { + targetSubnetType = ec2.SubnetType.PRIVATE_ISOLATED; + } else if (vpc.privateSubnets.length) { + targetSubnetType = ec2.SubnetType.PRIVATE_WITH_EGRESS; + } else { + throw new Error('Error - the OpenSearch Service domain can only be deployed in Isolated or Private subnets'); + } + + const subnetSelector = { + onePerAz: true, + subnetType: targetSubnetType + }; + + return vpc.selectSubnets(subnetSelector).subnetIds; +} + +function createClusterConfiguration(numberOfAzs?: number): opensearch.CfnDomain.ClusterConfigProperty { + return { + dedicatedMasterEnabled: true, + dedicatedMasterCount: 3, + zoneAwarenessEnabled: true, + zoneAwarenessConfig: { + availabilityZoneCount: numberOfAzs + }, + instanceCount: numberOfAzs, + }; +} + +function createKibanaCognitoRole( + scope: Construct, + userPool: cognito.UserPool, + identitypool: cognito.CfnIdentityPool, + domainName: string +): iam.Role { + // Setup the IAM Role & policy for the OpenSearch Service to configure Cognito User pool and Identity pool + const cognitoKibanaConfigureRole = new iam.Role( + scope, + "CognitoKibanaConfigureRole", + { + assumedBy: new iam.ServicePrincipal("es.amazonaws.com"), + } + ); + + const cognitoKibanaConfigureRolePolicy = new iam.Policy( + scope, + "CognitoKibanaConfigureRolePolicy", + { + statements: [ + new iam.PolicyStatement({ + actions: [ + "cognito-idp:DescribeUserPool", + "cognito-idp:CreateUserPoolClient", + "cognito-idp:DeleteUserPoolClient", + "cognito-idp:DescribeUserPoolClient", + "cognito-idp:AdminInitiateAuth", + "cognito-idp:AdminUserGlobalSignOut", + "cognito-idp:ListUserPoolClients", + "cognito-identity:DescribeIdentityPool", + "cognito-identity:UpdateIdentityPool", + "cognito-identity:SetIdentityPoolRoles", + "cognito-identity:GetIdentityPoolRoles", + "es:UpdateDomainConfig", + ], + resources: [ + userPool.userPoolArn, + `arn:aws:cognito-identity:${cdk.Aws.REGION}:${cdk.Aws.ACCOUNT_ID}:identitypool/${identitypool.ref}`, + `arn:aws:es:${cdk.Aws.REGION}:${cdk.Aws.ACCOUNT_ID}:domain/${domainName}`, + ], + }), + new iam.PolicyStatement({ + actions: ["iam:PassRole"], + conditions: { + StringLike: { + "iam:PassedToService": "cognito-identity.amazonaws.com", + }, + }, + resources: [cognitoKibanaConfigureRole.roleArn], + }), + ], + } + ); + + cognitoKibanaConfigureRolePolicy.attachToRole(cognitoKibanaConfigureRole); + return cognitoKibanaConfigureRole; +} diff --git a/source/patterns/@aws-solutions-constructs/core/test/opensearch-helper.test.ts b/source/patterns/@aws-solutions-constructs/core/test/opensearch-helper.test.ts new file mode 100644 index 000000000..08354bc00 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/core/test/opensearch-helper.test.ts @@ -0,0 +1,432 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { Stack } from 'aws-cdk-lib'; +import * as opensearch from 'aws-cdk-lib/aws-opensearchservice'; +import * as defaults from '../index'; +import '@aws-cdk/assert/jest'; +import * as iam from 'aws-cdk-lib/aws-iam'; +import * as ec2 from 'aws-cdk-lib/aws-ec2'; + +function deployOpenSearch(stack: Stack, openSearchDomainName: string, clientDomainProps?: opensearch.CfnDomainProps, + lambdaRoleARN?: string, vpc?: ec2.IVpc): [opensearch.CfnDomain, iam.Role] { + const userpool = defaults.buildUserPool(stack); + const userpoolclient = defaults.buildUserPoolClient(stack, userpool, { + userPoolClientName: 'test', + userPool: userpool + }); + + const identitypool = defaults.buildIdentityPool(stack, userpool, userpoolclient); + const cognitoAuthorizedRole = defaults.setupCognitoForElasticSearch(stack, 'test-domain', { + userpool, + userpoolclient, + identitypool + }); + + return defaults.buildOpenSearch(stack, { + userpool, + identitypool, + cognitoAuthorizedRoleARN: cognitoAuthorizedRole.roleArn, + serviceRoleARN: lambdaRoleARN ? lambdaRoleARN : undefined, + vpc, + openSearchDomainName, + clientDomainProps + }); +} + +function deployStack() { + return new Stack(undefined, undefined, { + env: { account: "123456789012", region: 'us-east-1' }, + }); +} + +test('Test override SnapshotOptions for buildOpenSearch', () => { + const stack = deployStack(); + + deployOpenSearch(stack, 'test-domain', { + snapshotOptions: { + automatedSnapshotStartHour: 5 + } + }); + + expect(stack).toHaveResource('AWS::OpenSearchService::Domain', { + AccessPolicies: { + Statement: [ + { + Action: "es:ESHttp*", + Effect: "Allow", + Principal: { + AWS: { + "Fn::GetAtt": [ + "CognitoAuthorizedRole14E74FE0", + "Arn" + ] + } + }, + Resource: { + "Fn::Join": [ + "", + [ + "arn:aws:es:", + { + Ref: "AWS::Region" + }, + ":", + { + Ref: "AWS::AccountId" + }, + ":domain/test-domain/*" + ] + ] + } + } + ], + Version: "2012-10-17" + }, + CognitoOptions: { + Enabled: true, + IdentityPoolId: { + Ref: "CognitoIdentityPool" + }, + RoleArn: { + "Fn::GetAtt": [ + "CognitoKibanaConfigureRole62CCE76A", + "Arn" + ] + }, + UserPoolId: { + Ref: "CognitoUserPool53E37E69" + } + }, + DomainName: "test-domain", + EBSOptions: { + EBSEnabled: true, + VolumeSize: 10 + }, + ClusterConfig: { + DedicatedMasterCount: 3, + DedicatedMasterEnabled: true, + InstanceCount: 3, + ZoneAwarenessConfig: { + AvailabilityZoneCount: 3 + }, + ZoneAwarenessEnabled: true + }, + EngineVersion: "OpenSearch_1.3", + EncryptionAtRestOptions: { + Enabled: true + }, + NodeToNodeEncryptionOptions: { + Enabled: true + }, + SnapshotOptions: { + AutomatedSnapshotStartHour: 5 + } + }); +}); + +test('Test VPC with 1 AZ, Zone Awareness Disabled', () => { + const stack = deployStack(); + + const vpc = defaults.getTestVpc(stack, false); + + deployOpenSearch(stack, 'test-domain', { + clusterConfig: { + dedicatedMasterEnabled: true, + dedicatedMasterCount: 3, + instanceCount: 3, + zoneAwarenessEnabled: false + } + }, undefined, vpc); + + expect(stack).toHaveResourceLike('AWS::OpenSearchService::Domain', { + DomainName: "test-domain", + ClusterConfig: { + DedicatedMasterCount: 3, + DedicatedMasterEnabled: true, + InstanceCount: 3, + ZoneAwarenessEnabled: false + } + }); +}); + +test('Test VPC with 2 AZ, Zone Awareness Enabled', () => { + // If no environment is specified, a VPC will use 2 AZs by default. + // If an environment is specified, a VPC will use 3 AZs by default. + const stack = new Stack(undefined, undefined, {}); + + const vpc: ec2.IVpc = defaults.getTestVpc(stack, false); + + deployOpenSearch(stack, 'test-domain', {}, undefined, vpc); + + expect(stack).toHaveResourceLike('AWS::OpenSearchService::Domain', { + DomainName: "test-domain", + ClusterConfig: { + DedicatedMasterCount: 3, + DedicatedMasterEnabled: true, + InstanceCount: 2, + ZoneAwarenessEnabled: true + } + }); +}); + +test('Test VPC with 3 AZ, Zone Awareness Enabled', () => { + // If no environment is specified, a VPC will use 2 AZs by default. + // If an environment is specified, a VPC will use 3 AZs by default. + const stack = deployStack(); + + const vpc: ec2.IVpc = defaults.getTestVpc(stack); + + deployOpenSearch(stack, 'test-domain', {}, undefined, vpc); + + expect(stack).toHaveResourceLike('AWS::OpenSearchService::Domain', { + DomainName: "test-domain", + ClusterConfig: { + DedicatedMasterCount: 3, + DedicatedMasterEnabled: true, + InstanceCount: 3, + ZoneAwarenessEnabled: true + } + }); +}); + +test('Test deployment with an existing private VPC', () => { + const stack = deployStack(); + + const vpc = new ec2.Vpc(stack, 'existing-private-vpc-test', { + natGateways: 1, + subnetConfiguration: [ + { + cidrMask: 24, + name: 'application', + subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS, + }, + { + cidrMask: 24, + name: "public", + subnetType: ec2.SubnetType.PUBLIC, + } + ] + }); + + deployOpenSearch(stack, 'test-domain', {}, undefined, vpc); + + expect(stack).toHaveResourceLike('AWS::OpenSearchService::Domain', { + DomainName: "test-domain", + ClusterConfig: { + DedicatedMasterCount: 3, + DedicatedMasterEnabled: true, + InstanceCount: 3, + ZoneAwarenessEnabled: true + } + }); +}); + +test('Test error thrown with no private subnet configurations', () => { + const stack = deployStack(); + + const vpc = defaults.buildVpc(stack, { + defaultVpcProps: { + subnetConfiguration: [ + { + cidrMask: 18, + name: "public", + subnetType: ec2.SubnetType.PUBLIC, + } + ] + } + }); + + const app = () => { + deployOpenSearch(stack, 'test-domain', {}, undefined, vpc); + }; + + expect(app).toThrowError('Error - the OpenSearch Service domain can only be deployed in Isolated or Private subnets'); +}); + +test('Test engine version override for buildOpenSearch', () => { + const stack = deployStack(); + + deployOpenSearch(stack, 'test-domain', { + engineVersion: 'OpenSearch_1.0' + }); + + expect(stack).toHaveResource('AWS::OpenSearchService::Domain', { + AccessPolicies: { + Statement: [ + { + Action: "es:ESHttp*", + Effect: "Allow", + Principal: { + AWS: { + "Fn::GetAtt": [ + "CognitoAuthorizedRole14E74FE0", + "Arn" + ] + } + }, + Resource: { + "Fn::Join": [ + "", + [ + "arn:aws:es:", + { + Ref: "AWS::Region" + }, + ":", + { + Ref: "AWS::AccountId" + }, + ":domain/test-domain/*" + ] + ] + } + } + ], + Version: "2012-10-17" + }, + CognitoOptions: { + Enabled: true, + IdentityPoolId: { + Ref: "CognitoIdentityPool" + }, + RoleArn: { + "Fn::GetAtt": [ + "CognitoKibanaConfigureRole62CCE76A", + "Arn" + ] + }, + UserPoolId: { + Ref: "CognitoUserPool53E37E69" + } + }, + DomainName: "test-domain", + EBSOptions: { + EBSEnabled: true, + VolumeSize: 10 + }, + ClusterConfig: { + DedicatedMasterCount: 3, + DedicatedMasterEnabled: true, + InstanceCount: 3, + ZoneAwarenessConfig: { + AvailabilityZoneCount: 3 + }, + ZoneAwarenessEnabled: true + }, + EngineVersion: "OpenSearch_1.0", + EncryptionAtRestOptions: { + Enabled: true + }, + NodeToNodeEncryptionOptions: { + Enabled: true + }, + SnapshotOptions: { + AutomatedSnapshotStartHour: 1 + } + }); + +}); + +test('Test deployment with lambdaRoleARN', () => { + const stack = deployStack(); + + deployOpenSearch(stack, 'test-domain', {}, 'arn:aws:us-east-1:mylambdaRoleARN'); + + expect(stack).toHaveResource('AWS::OpenSearchService::Domain', { + AccessPolicies: { + Statement: [ + { + Action: "es:ESHttp*", + Effect: "Allow", + Principal: { + AWS: [ + { + "Fn::GetAtt": [ + "CognitoAuthorizedRole14E74FE0", + "Arn" + ] + }, + "arn:aws:us-east-1:mylambdaRoleARN" + ] + }, + Resource: { + "Fn::Join": [ + "", + [ + "arn:aws:es:", + { + Ref: "AWS::Region" + }, + ":", + { + Ref: "AWS::AccountId" + }, + ":domain/test-domain/*" + ] + ] + } + } + ], + Version: "2012-10-17" + }, + CognitoOptions: { + Enabled: true, + IdentityPoolId: { + Ref: "CognitoIdentityPool" + }, + RoleArn: { + "Fn::GetAtt": [ + "CognitoKibanaConfigureRole62CCE76A", + "Arn" + ] + }, + UserPoolId: { + Ref: "CognitoUserPool53E37E69" + } + }, + DomainName: "test-domain", + EBSOptions: { + EBSEnabled: true, + VolumeSize: 10 + }, + ClusterConfig: { + DedicatedMasterCount: 3, + DedicatedMasterEnabled: true, + InstanceCount: 3, + ZoneAwarenessConfig: { + AvailabilityZoneCount: 3 + }, + ZoneAwarenessEnabled: true + }, + EngineVersion: "OpenSearch_1.3", + EncryptionAtRestOptions: { + Enabled: true + }, + NodeToNodeEncryptionOptions: { + Enabled: true + }, + SnapshotOptions: { + AutomatedSnapshotStartHour: 1 + } + }); + +}); + +test('Count OpenSearch CloudWatch alarms', () => { + const stack = new Stack(); + deployOpenSearch(stack, 'test-domain'); + const cwList = defaults.buildOpenSearchCWAlarms(stack); + + expect(cwList.length).toEqual(9); +}); From d926fd2e7fba33369f5c08bef5f9149428fc24e9 Mon Sep 17 00:00:00 2001 From: mickychetta Date: Tue, 4 Oct 2022 17:52:10 +0000 Subject: [PATCH 7/9] fixed prop description --- .../aws-lambda-opensearch/lib/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/lib/index.ts b/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/lib/index.ts index de61df4a6..fd4ca461f 100644 --- a/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/lib/index.ts +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/lib/index.ts @@ -22,7 +22,7 @@ import * as defaults from '@aws-solutions-constructs/core'; import { Construct } from 'constructs'; /** - * @summary The properties for the CognitoToApiGatewayToLambda Construct + * @summary The properties for the LambdaToOpenSearch Construct */ export interface LambdaToOpenSearchProps { /** @@ -52,7 +52,7 @@ export interface LambdaToOpenSearchProps { /** * Optional Amazon Cognito domain name. If omitted the Amazon Cognito domain will default to the OpenSearch Service domain name. * - * @default - None + * @default - the OpenSearch Service domain name */ readonly cognitoDomainName?: string; /** From cdfc28957c3d639e5bdc87dd8c0b222bed47bb96 Mon Sep 17 00:00:00 2001 From: mickychetta Date: Wed, 5 Oct 2022 00:12:48 +0000 Subject: [PATCH 8/9] removed kibana text in opensearch construct --- .../lib/index.ts | 27 +++--------- ...loyFunctionWithClusterConfig.expected.json | 2 +- ...eployFunctionWithExistingVpc.expected.json | 2 +- ...g.deployFunctionWithVpcProps.expected.json | 2 +- .../integ.deployToFiveZones.expected.json | 2 +- .../integ.disabledZoneAwareness.expected.json | 2 +- .../test/integ.domain-arguments.expected.json | 2 +- .../test/integ.no-arguments.expected.json | 2 +- .../test/lambda/index.js | 17 ++++---- .../aws-lambda-opensearch/.eslintignore | 2 +- .../aws-lambda-opensearch/.gitignore | 2 +- .../aws-lambda-opensearch/.npmignore | 2 +- .../aws-lambda-opensearch/lib/index.ts | 4 +- .../test/integ.cluster-config.expected.json | 14 +++---- ...nteg.disabled-zone-awareness.expected.json | 14 +++---- .../test/integ.domain-arguments.expected.json | 14 +++---- .../test/integ.existing-vpc.expected.json | 14 +++---- .../test/integ.no-arguments.expected.json | 14 +++---- .../test/integ.vpc-props.expected.json | 14 +++---- .../test/lambda/index.js | 8 ++-- .../core/lib/cognito-helper.ts | 6 +-- .../core/lib/elasticsearch-helper.ts | 20 +-------- .../core/lib/opensearch-defaults.ts | 3 ++ .../core/lib/opensearch-helper.ts | 42 ++++++------------- .../core/lib/vpc-helper.ts | 19 +++++++++ .../core/test/congnito-helper.test.ts | 4 +- .../core/test/elasticsearch-helper.test.ts | 4 +- .../core/test/opensearch-helper.test.ts | 10 ++--- 28 files changed, 121 insertions(+), 147 deletions(-) diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-elasticsearch-kibana/lib/index.ts b/source/patterns/@aws-solutions-constructs/aws-lambda-elasticsearch-kibana/lib/index.ts index 928e213d0..367d67aa6 100644 --- a/source/patterns/@aws-solutions-constructs/aws-lambda-elasticsearch-kibana/lib/index.ts +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-elasticsearch-kibana/lib/index.ts @@ -18,12 +18,11 @@ import * as cognito from 'aws-cdk-lib/aws-cognito'; import * as defaults from '@aws-solutions-constructs/core'; // Note: To ensure CDKv2 compatibility, keep the import statement for Construct separate import { Construct } from 'constructs'; -import { Role } from 'aws-cdk-lib/aws-iam'; import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch'; import * as ec2 from 'aws-cdk-lib/aws-ec2'; /** - * @summary The properties for the CognitoToApiGatewayToLambda Construct + * @summary The properties for the LambdaToElasticSearchAndKibana Construct */ export interface LambdaToElasticSearchAndKibanaProps { /** @@ -99,10 +98,10 @@ export class LambdaToElasticSearchAndKibana extends Construct { public readonly vpc?: ec2.IVpc; /** - * @summary Constructs a new instance of the CognitoToApiGatewayToLambda class. - * @param {cdk.App} scope - represents the scope for all the resources. + * @summary Constructs a new instance of the LambdaToElasticSearchAndKibana class. + * @param {Constructs} scope - represents the scope for all the resources. * @param {string} id - this is a a scope-unique id. - * @param {CognitoToApiGatewayToLambdaProps} props - user provided props for the construct + * @param {LambdaToElasticSearchAndKibanaProps} props - user provided props for the construct * @since 0.8.0 * @access public */ @@ -146,7 +145,7 @@ export class LambdaToElasticSearchAndKibana extends Construct { let cognitoAuthorizedRole: iam.Role; [this.userPool, this.userPoolClient, this.identityPool, cognitoAuthorizedRole] = - this.setupCognito(this, props.cognitoDomainName ?? props.domainName); + defaults.buildCognitoForSearchService(this, props.cognitoDomainName ?? props.domainName); const buildElasticSearchProps: any = { userpool: this.userPool, @@ -174,18 +173,4 @@ export class LambdaToElasticSearchAndKibana extends Construct { this.cloudwatchAlarms = defaults.buildElasticSearchCWAlarms(this); } } - - setupCognito(scope: Construct, domainName: string): [cognito.UserPool, cognito.UserPoolClient, cognito.CfnIdentityPool, iam.Role] { - const userPool = defaults.buildUserPool(scope); - const userPoolClient = defaults.buildUserPoolClient(scope, userPool); - const identityPool = defaults.buildIdentityPool(scope, userPool, userPoolClient); - - const cognitoAuthorizedRole: Role = defaults.setupCognitoForElasticSearch(scope, domainName, { - userpool: userPool, - identitypool: identityPool, - userpoolclient: userPoolClient - }); - - return [userPool, userPoolClient, identityPool, cognitoAuthorizedRole]; - } -} \ No newline at end of file +} diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-elasticsearch-kibana/test/integ.deployFunctionWithClusterConfig.expected.json b/source/patterns/@aws-solutions-constructs/aws-lambda-elasticsearch-kibana/test/integ.deployFunctionWithClusterConfig.expected.json index 645ed472b..9b687d5be 100644 --- a/source/patterns/@aws-solutions-constructs/aws-lambda-elasticsearch-kibana/test/integ.deployFunctionWithClusterConfig.expected.json +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-elasticsearch-kibana/test/integ.deployFunctionWithClusterConfig.expected.json @@ -137,7 +137,7 @@ "S3Bucket": { "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" }, - "S3Key": "67a9971e29baab2bde3043bb70ce5b53318b95429a1ce9b189cf65223e8682db.zip" + "S3Key": "35bbbc7b04b21f225891f139adf234188f348ebad5f4bbc2c06edf8aa3784c97.zip" }, "Role": { "Fn::GetAtt": [ diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-elasticsearch-kibana/test/integ.deployFunctionWithExistingVpc.expected.json b/source/patterns/@aws-solutions-constructs/aws-lambda-elasticsearch-kibana/test/integ.deployFunctionWithExistingVpc.expected.json index 7392d531e..a3b86b904 100644 --- a/source/patterns/@aws-solutions-constructs/aws-lambda-elasticsearch-kibana/test/integ.deployFunctionWithExistingVpc.expected.json +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-elasticsearch-kibana/test/integ.deployFunctionWithExistingVpc.expected.json @@ -804,7 +804,7 @@ "Properties": { "Code": { "S3Bucket": "cdk-hnb659fds-assets-12345678-test-region", - "S3Key": "67a9971e29baab2bde3043bb70ce5b53318b95429a1ce9b189cf65223e8682db.zip" + "S3Key": "35bbbc7b04b21f225891f139adf234188f348ebad5f4bbc2c06edf8aa3784c97.zip" }, "Role": { "Fn::GetAtt": [ diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-elasticsearch-kibana/test/integ.deployFunctionWithVpcProps.expected.json b/source/patterns/@aws-solutions-constructs/aws-lambda-elasticsearch-kibana/test/integ.deployFunctionWithVpcProps.expected.json index c53c597ab..3a3b0ea81 100644 --- a/source/patterns/@aws-solutions-constructs/aws-lambda-elasticsearch-kibana/test/integ.deployFunctionWithVpcProps.expected.json +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-elasticsearch-kibana/test/integ.deployFunctionWithVpcProps.expected.json @@ -135,7 +135,7 @@ "Properties": { "Code": { "S3Bucket": "cdk-hnb659fds-assets-12345678-test-region", - "S3Key": "67a9971e29baab2bde3043bb70ce5b53318b95429a1ce9b189cf65223e8682db.zip" + "S3Key": "35bbbc7b04b21f225891f139adf234188f348ebad5f4bbc2c06edf8aa3784c97.zip" }, "Role": { "Fn::GetAtt": [ diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-elasticsearch-kibana/test/integ.deployToFiveZones.expected.json b/source/patterns/@aws-solutions-constructs/aws-lambda-elasticsearch-kibana/test/integ.deployToFiveZones.expected.json index c5b66132e..430146924 100644 --- a/source/patterns/@aws-solutions-constructs/aws-lambda-elasticsearch-kibana/test/integ.deployToFiveZones.expected.json +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-elasticsearch-kibana/test/integ.deployToFiveZones.expected.json @@ -137,7 +137,7 @@ "S3Bucket": { "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" }, - "S3Key": "67a9971e29baab2bde3043bb70ce5b53318b95429a1ce9b189cf65223e8682db.zip" + "S3Key": "35bbbc7b04b21f225891f139adf234188f348ebad5f4bbc2c06edf8aa3784c97.zip" }, "Role": { "Fn::GetAtt": [ diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-elasticsearch-kibana/test/integ.disabledZoneAwareness.expected.json b/source/patterns/@aws-solutions-constructs/aws-lambda-elasticsearch-kibana/test/integ.disabledZoneAwareness.expected.json index 3040a00f1..952399fd2 100644 --- a/source/patterns/@aws-solutions-constructs/aws-lambda-elasticsearch-kibana/test/integ.disabledZoneAwareness.expected.json +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-elasticsearch-kibana/test/integ.disabledZoneAwareness.expected.json @@ -137,7 +137,7 @@ "S3Bucket": { "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" }, - "S3Key": "67a9971e29baab2bde3043bb70ce5b53318b95429a1ce9b189cf65223e8682db.zip" + "S3Key": "35bbbc7b04b21f225891f139adf234188f348ebad5f4bbc2c06edf8aa3784c97.zip" }, "Role": { "Fn::GetAtt": [ diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-elasticsearch-kibana/test/integ.domain-arguments.expected.json b/source/patterns/@aws-solutions-constructs/aws-lambda-elasticsearch-kibana/test/integ.domain-arguments.expected.json index 2d971b722..207e58345 100644 --- a/source/patterns/@aws-solutions-constructs/aws-lambda-elasticsearch-kibana/test/integ.domain-arguments.expected.json +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-elasticsearch-kibana/test/integ.domain-arguments.expected.json @@ -96,7 +96,7 @@ "S3Bucket": { "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" }, - "S3Key": "67a9971e29baab2bde3043bb70ce5b53318b95429a1ce9b189cf65223e8682db.zip" + "S3Key": "35bbbc7b04b21f225891f139adf234188f348ebad5f4bbc2c06edf8aa3784c97.zip" }, "Role": { "Fn::GetAtt": [ diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-elasticsearch-kibana/test/integ.no-arguments.expected.json b/source/patterns/@aws-solutions-constructs/aws-lambda-elasticsearch-kibana/test/integ.no-arguments.expected.json index f2efe469d..37e2c00cd 100644 --- a/source/patterns/@aws-solutions-constructs/aws-lambda-elasticsearch-kibana/test/integ.no-arguments.expected.json +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-elasticsearch-kibana/test/integ.no-arguments.expected.json @@ -96,7 +96,7 @@ "S3Bucket": { "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" }, - "S3Key": "67a9971e29baab2bde3043bb70ce5b53318b95429a1ce9b189cf65223e8682db.zip" + "S3Key": "35bbbc7b04b21f225891f139adf234188f348ebad5f4bbc2c06edf8aa3784c97.zip" }, "Role": { "Fn::GetAtt": [ diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-elasticsearch-kibana/test/lambda/index.js b/source/patterns/@aws-solutions-constructs/aws-lambda-elasticsearch-kibana/test/lambda/index.js index 7a660ab80..5362a2cd1 100644 --- a/source/patterns/@aws-solutions-constructs/aws-lambda-elasticsearch-kibana/test/lambda/index.js +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-elasticsearch-kibana/test/lambda/index.js @@ -34,7 +34,7 @@ function postDocumentToES(doc, context) { var body = ''; httpResp.on('data', function (chunk) { body += chunk; - }); + }); httpResp.on('end', function (chunk) { console.log('All movie records added to ES.'); context.succeed(); @@ -47,10 +47,13 @@ function postDocumentToES(doc, context) { exports.handler = (event, context) => { console.log('Received event:', JSON.stringify(event, null, 2)); - postDocumentToES("{ \"title\": \"Spirited Away\" }", context); - return { -      statusCode: 200, -      headers: { 'Content-Type': 'text/plain' }, -      body: `Hello from Project Vesper! You've hit ${event.path}\n` -    }; + postDocumentToOpenSearch("{ \"title\": \"Moby Dick\" }", context); + postDocumentToOpenSearch("{ \"title\": \"A Tale of Two Cities\" }", context); + postDocumentToOpenSearch("{ \"title\": \"The Phantom of the Opera\" }", context); + + return { + statusCode: 200, + headers: { 'Content-Type': 'text/plain' }, + body: `Hello from AWS Solutions Constructs!\n` + }; }; \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/.eslintignore b/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/.eslintignore index 0819e2e65..340869a08 100644 --- a/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/.eslintignore +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/.eslintignore @@ -2,4 +2,4 @@ lib/*.js test/*.js *.d.ts coverage -test/lambda/index.js \ No newline at end of file +test/lambda/index.js diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/.gitignore b/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/.gitignore index 8626f2274..bd415aef8 100644 --- a/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/.gitignore +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/.gitignore @@ -13,4 +13,4 @@ dist coverage .nycrc .LAST_PACKAGE -*.snk \ No newline at end of file +*.snk diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/.npmignore b/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/.npmignore index f66791629..1ce9af351 100644 --- a/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/.npmignore +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/.npmignore @@ -18,4 +18,4 @@ dist !.jsii # Include .jsii -!.jsii \ No newline at end of file +!.jsii diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/lib/index.ts b/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/lib/index.ts index fd4ca461f..40b12df4a 100644 --- a/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/lib/index.ts +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/lib/index.ts @@ -99,7 +99,7 @@ export class LambdaToOpenSearch extends Construct { /** * @summary Constructs a new instance of the LambdaToOpenSearch class. - * @param {cdk.App} scope - represents the scope for all the resources. + * @param {Construct} scope - represents the scope for all the resources. * @param {string} id - this is a a scope-unique id. * @param {LambdaToOpenSearchProps} props - user provided props for the construct * @since 0.8.0 @@ -145,7 +145,7 @@ export class LambdaToOpenSearch extends Construct { let cognitoAuthorizedRole: iam.Role; [this.userPool, this.userPoolClient, this.identityPool, cognitoAuthorizedRole] = - defaults.setupOpenSearchCognito(this, props.cognitoDomainName ?? props.openSearchDomainName); + defaults.buildCognitoForSearchService(this, props.cognitoDomainName ?? props.openSearchDomainName); const buildOpenSearchProps: any = { userpool: this.userPool, diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/test/integ.cluster-config.expected.json b/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/test/integ.cluster-config.expected.json index a5105b28f..8624352f7 100644 --- a/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/test/integ.cluster-config.expected.json +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/test/integ.cluster-config.expected.json @@ -137,7 +137,7 @@ "S3Bucket": { "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" }, - "S3Key": "466cbfc6653782bd2707dec5bc5c58005c75a8bff3660a58eefa57d667120240.zip" + "S3Key": "abbc4eca9e7ddabc31da3ce83159e6eee8e72e2c358ab8af0711044514c41290.zip" }, "Role": { "Fn::GetAtt": [ @@ -374,7 +374,7 @@ } } }, - "testlambdaopensearchCognitoKibanaConfigureRole9623271F": { + "testlambdaopensearchCognitoDashboardConfigureRole1F2B7B7A": { "Type": "AWS::IAM::Role", "Properties": { "AssumeRolePolicyDocument": { @@ -391,7 +391,7 @@ } } }, - "testlambdaopensearchCognitoKibanaConfigureRolePolicyE9D9C211": { + "testlambdaopensearchCognitoDashboardConfigureRolePolicyC9C6A6A2": { "Type": "AWS::IAM::Policy", "Properties": { "PolicyDocument": { @@ -466,7 +466,7 @@ "Effect": "Allow", "Resource": { "Fn::GetAtt": [ - "testlambdaopensearchCognitoKibanaConfigureRole9623271F", + "testlambdaopensearchCognitoDashboardConfigureRole1F2B7B7A", "Arn" ] } @@ -474,10 +474,10 @@ ], "Version": "2012-10-17" }, - "PolicyName": "testlambdaopensearchCognitoKibanaConfigureRolePolicyE9D9C211", + "PolicyName": "testlambdaopensearchCognitoDashboardConfigureRolePolicyC9C6A6A2", "Roles": [ { - "Ref": "testlambdaopensearchCognitoKibanaConfigureRole9623271F" + "Ref": "testlambdaopensearchCognitoDashboardConfigureRole1F2B7B7A" } ] } @@ -542,7 +542,7 @@ }, "RoleArn": { "Fn::GetAtt": [ - "testlambdaopensearchCognitoKibanaConfigureRole9623271F", + "testlambdaopensearchCognitoDashboardConfigureRole1F2B7B7A", "Arn" ] }, diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/test/integ.disabled-zone-awareness.expected.json b/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/test/integ.disabled-zone-awareness.expected.json index 51d046c24..e936bc551 100644 --- a/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/test/integ.disabled-zone-awareness.expected.json +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/test/integ.disabled-zone-awareness.expected.json @@ -137,7 +137,7 @@ "S3Bucket": { "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" }, - "S3Key": "466cbfc6653782bd2707dec5bc5c58005c75a8bff3660a58eefa57d667120240.zip" + "S3Key": "abbc4eca9e7ddabc31da3ce83159e6eee8e72e2c358ab8af0711044514c41290.zip" }, "Role": { "Fn::GetAtt": [ @@ -370,7 +370,7 @@ } } }, - "testlambdaopensearchCognitoKibanaConfigureRole9623271F": { + "testlambdaopensearchCognitoDashboardConfigureRole1F2B7B7A": { "Type": "AWS::IAM::Role", "Properties": { "AssumeRolePolicyDocument": { @@ -387,7 +387,7 @@ } } }, - "testlambdaopensearchCognitoKibanaConfigureRolePolicyE9D9C211": { + "testlambdaopensearchCognitoDashboardConfigureRolePolicyC9C6A6A2": { "Type": "AWS::IAM::Policy", "Properties": { "PolicyDocument": { @@ -462,7 +462,7 @@ "Effect": "Allow", "Resource": { "Fn::GetAtt": [ - "testlambdaopensearchCognitoKibanaConfigureRole9623271F", + "testlambdaopensearchCognitoDashboardConfigureRole1F2B7B7A", "Arn" ] } @@ -470,10 +470,10 @@ ], "Version": "2012-10-17" }, - "PolicyName": "testlambdaopensearchCognitoKibanaConfigureRolePolicyE9D9C211", + "PolicyName": "testlambdaopensearchCognitoDashboardConfigureRolePolicyC9C6A6A2", "Roles": [ { - "Ref": "testlambdaopensearchCognitoKibanaConfigureRole9623271F" + "Ref": "testlambdaopensearchCognitoDashboardConfigureRole1F2B7B7A" } ] } @@ -535,7 +535,7 @@ }, "RoleArn": { "Fn::GetAtt": [ - "testlambdaopensearchCognitoKibanaConfigureRole9623271F", + "testlambdaopensearchCognitoDashboardConfigureRole1F2B7B7A", "Arn" ] }, diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/test/integ.domain-arguments.expected.json b/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/test/integ.domain-arguments.expected.json index 3c73408ee..3bb455180 100644 --- a/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/test/integ.domain-arguments.expected.json +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/test/integ.domain-arguments.expected.json @@ -96,7 +96,7 @@ "S3Bucket": { "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" }, - "S3Key": "466cbfc6653782bd2707dec5bc5c58005c75a8bff3660a58eefa57d667120240.zip" + "S3Key": "abbc4eca9e7ddabc31da3ce83159e6eee8e72e2c358ab8af0711044514c41290.zip" }, "Role": { "Fn::GetAtt": [ @@ -313,7 +313,7 @@ } } }, - "testlambdaopensearchCognitoKibanaConfigureRole9623271F": { + "testlambdaopensearchCognitoDashboardConfigureRole1F2B7B7A": { "Type": "AWS::IAM::Role", "Properties": { "AssumeRolePolicyDocument": { @@ -330,7 +330,7 @@ } } }, - "testlambdaopensearchCognitoKibanaConfigureRolePolicyE9D9C211": { + "testlambdaopensearchCognitoDashboardConfigureRolePolicyC9C6A6A2": { "Type": "AWS::IAM::Policy", "Properties": { "PolicyDocument": { @@ -405,7 +405,7 @@ "Effect": "Allow", "Resource": { "Fn::GetAtt": [ - "testlambdaopensearchCognitoKibanaConfigureRole9623271F", + "testlambdaopensearchCognitoDashboardConfigureRole1F2B7B7A", "Arn" ] } @@ -413,10 +413,10 @@ ], "Version": "2012-10-17" }, - "PolicyName": "testlambdaopensearchCognitoKibanaConfigureRolePolicyE9D9C211", + "PolicyName": "testlambdaopensearchCognitoDashboardConfigureRolePolicyC9C6A6A2", "Roles": [ { - "Ref": "testlambdaopensearchCognitoKibanaConfigureRole9623271F" + "Ref": "testlambdaopensearchCognitoDashboardConfigureRole1F2B7B7A" } ] } @@ -481,7 +481,7 @@ }, "RoleArn": { "Fn::GetAtt": [ - "testlambdaopensearchCognitoKibanaConfigureRole9623271F", + "testlambdaopensearchCognitoDashboardConfigureRole1F2B7B7A", "Arn" ] }, diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/test/integ.existing-vpc.expected.json b/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/test/integ.existing-vpc.expected.json index b21beaacc..d9eb71c21 100644 --- a/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/test/integ.existing-vpc.expected.json +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/test/integ.existing-vpc.expected.json @@ -804,7 +804,7 @@ "Properties": { "Code": { "S3Bucket": "cdk-hnb659fds-assets-12345678-test-region", - "S3Key": "466cbfc6653782bd2707dec5bc5c58005c75a8bff3660a58eefa57d667120240.zip" + "S3Key": "abbc4eca9e7ddabc31da3ce83159e6eee8e72e2c358ab8af0711044514c41290.zip" }, "Role": { "Fn::GetAtt": [ @@ -1048,7 +1048,7 @@ } } }, - "testlambdaelasticsearchkibana4CognitoKibanaConfigureRole6A058B80": { + "testlambdaelasticsearchkibana4CognitoDashboardConfigureRoleB36C775C": { "Type": "AWS::IAM::Role", "Properties": { "AssumeRolePolicyDocument": { @@ -1065,7 +1065,7 @@ } } }, - "testlambdaelasticsearchkibana4CognitoKibanaConfigureRolePolicy1615E937": { + "testlambdaelasticsearchkibana4CognitoDashboardConfigureRolePolicy1D82A101": { "Type": "AWS::IAM::Policy", "Properties": { "PolicyDocument": { @@ -1140,7 +1140,7 @@ "Effect": "Allow", "Resource": { "Fn::GetAtt": [ - "testlambdaelasticsearchkibana4CognitoKibanaConfigureRole6A058B80", + "testlambdaelasticsearchkibana4CognitoDashboardConfigureRoleB36C775C", "Arn" ] } @@ -1148,10 +1148,10 @@ ], "Version": "2012-10-17" }, - "PolicyName": "testlambdaelasticsearchkibana4CognitoKibanaConfigureRolePolicy1615E937", + "PolicyName": "testlambdaelasticsearchkibana4CognitoDashboardConfigureRolePolicy1D82A101", "Roles": [ { - "Ref": "testlambdaelasticsearchkibana4CognitoKibanaConfigureRole6A058B80" + "Ref": "testlambdaelasticsearchkibana4CognitoDashboardConfigureRoleB36C775C" } ] } @@ -1216,7 +1216,7 @@ }, "RoleArn": { "Fn::GetAtt": [ - "testlambdaelasticsearchkibana4CognitoKibanaConfigureRole6A058B80", + "testlambdaelasticsearchkibana4CognitoDashboardConfigureRoleB36C775C", "Arn" ] }, diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/test/integ.no-arguments.expected.json b/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/test/integ.no-arguments.expected.json index b65887f99..52cb2b7e0 100644 --- a/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/test/integ.no-arguments.expected.json +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/test/integ.no-arguments.expected.json @@ -96,7 +96,7 @@ "S3Bucket": { "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" }, - "S3Key": "466cbfc6653782bd2707dec5bc5c58005c75a8bff3660a58eefa57d667120240.zip" + "S3Key": "abbc4eca9e7ddabc31da3ce83159e6eee8e72e2c358ab8af0711044514c41290.zip" }, "Role": { "Fn::GetAtt": [ @@ -313,7 +313,7 @@ } } }, - "testlambdaopensearchCognitoKibanaConfigureRole9623271F": { + "testlambdaopensearchCognitoDashboardConfigureRole1F2B7B7A": { "Type": "AWS::IAM::Role", "Properties": { "AssumeRolePolicyDocument": { @@ -330,7 +330,7 @@ } } }, - "testlambdaopensearchCognitoKibanaConfigureRolePolicyE9D9C211": { + "testlambdaopensearchCognitoDashboardConfigureRolePolicyC9C6A6A2": { "Type": "AWS::IAM::Policy", "Properties": { "PolicyDocument": { @@ -405,7 +405,7 @@ "Effect": "Allow", "Resource": { "Fn::GetAtt": [ - "testlambdaopensearchCognitoKibanaConfigureRole9623271F", + "testlambdaopensearchCognitoDashboardConfigureRole1F2B7B7A", "Arn" ] } @@ -413,10 +413,10 @@ ], "Version": "2012-10-17" }, - "PolicyName": "testlambdaopensearchCognitoKibanaConfigureRolePolicyE9D9C211", + "PolicyName": "testlambdaopensearchCognitoDashboardConfigureRolePolicyC9C6A6A2", "Roles": [ { - "Ref": "testlambdaopensearchCognitoKibanaConfigureRole9623271F" + "Ref": "testlambdaopensearchCognitoDashboardConfigureRole1F2B7B7A" } ] } @@ -481,7 +481,7 @@ }, "RoleArn": { "Fn::GetAtt": [ - "testlambdaopensearchCognitoKibanaConfigureRole9623271F", + "testlambdaopensearchCognitoDashboardConfigureRole1F2B7B7A", "Arn" ] }, diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/test/integ.vpc-props.expected.json b/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/test/integ.vpc-props.expected.json index c459a8eee..d84589b6f 100644 --- a/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/test/integ.vpc-props.expected.json +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/test/integ.vpc-props.expected.json @@ -135,7 +135,7 @@ "Properties": { "Code": { "S3Bucket": "cdk-hnb659fds-assets-12345678-test-region", - "S3Key": "466cbfc6653782bd2707dec5bc5c58005c75a8bff3660a58eefa57d667120240.zip" + "S3Key": "abbc4eca9e7ddabc31da3ce83159e6eee8e72e2c358ab8af0711044514c41290.zip" }, "Role": { "Fn::GetAtt": [ @@ -376,7 +376,7 @@ } } }, - "testlambdaopensearchCognitoKibanaConfigureRole9623271F": { + "testlambdaopensearchCognitoDashboardConfigureRole1F2B7B7A": { "Type": "AWS::IAM::Role", "Properties": { "AssumeRolePolicyDocument": { @@ -393,7 +393,7 @@ } } }, - "testlambdaopensearchCognitoKibanaConfigureRolePolicyE9D9C211": { + "testlambdaopensearchCognitoDashboardConfigureRolePolicyC9C6A6A2": { "Type": "AWS::IAM::Policy", "Properties": { "PolicyDocument": { @@ -468,7 +468,7 @@ "Effect": "Allow", "Resource": { "Fn::GetAtt": [ - "testlambdaopensearchCognitoKibanaConfigureRole9623271F", + "testlambdaopensearchCognitoDashboardConfigureRole1F2B7B7A", "Arn" ] } @@ -476,10 +476,10 @@ ], "Version": "2012-10-17" }, - "PolicyName": "testlambdaopensearchCognitoKibanaConfigureRolePolicyE9D9C211", + "PolicyName": "testlambdaopensearchCognitoDashboardConfigureRolePolicyC9C6A6A2", "Roles": [ { - "Ref": "testlambdaopensearchCognitoKibanaConfigureRole9623271F" + "Ref": "testlambdaopensearchCognitoDashboardConfigureRole1F2B7B7A" } ] } @@ -544,7 +544,7 @@ }, "RoleArn": { "Fn::GetAtt": [ - "testlambdaopensearchCognitoKibanaConfigureRole9623271F", + "testlambdaopensearchCognitoDashboardConfigureRole1F2B7B7A", "Arn" ] }, diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/test/lambda/index.js b/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/test/lambda/index.js index b94797307..77e37ea45 100644 --- a/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/test/lambda/index.js +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/test/lambda/index.js @@ -48,13 +48,13 @@ function postDocumentToOpenSearch(doc, context) { exports.handler = (event, context) => { console.log('Received event:', JSON.stringify(event, null, 2)); - postDocumentToOpenSearch("{ \"title\": \"Spirited Away\" }", context); - postDocumentToOpenSearch("{ \"title\": \"Fast and Furious\" }", context); - postDocumentToOpenSearch("{ \"title\": \"Jurassic Park\" }", context); + postDocumentToOpenSearch("{ \"title\": \"Moby Dick\" }", context); + postDocumentToOpenSearch("{ \"title\": \"A Tale of Two Cities\" }", context); + postDocumentToOpenSearch("{ \"title\": \"The Phantom of the Opera\" }", context); return { statusCode: 200, headers: { 'Content-Type': 'text/plain' }, - body: `Hello from AWS Solutions Constructs You've hit ${event.path}\n` + body: `Hello from AWS Solutions Constructs!\n` }; }; \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/core/lib/cognito-helper.ts b/source/patterns/@aws-solutions-constructs/core/lib/cognito-helper.ts index 1c7a9b62d..125023040 100644 --- a/source/patterns/@aws-solutions-constructs/core/lib/cognito-helper.ts +++ b/source/patterns/@aws-solutions-constructs/core/lib/cognito-helper.ts @@ -77,7 +77,7 @@ export function buildIdentityPool(scope: Construct, userpool: cognito.UserPool, return idPool; } -export function setupCognitoForElasticSearch(scope: Construct, domainName: string, options: CognitoOptions): iam.Role { +export function setupCognitoForSearchService(scope: Construct, domainName: string, options: CognitoOptions): iam.Role { // Create the domain for Cognito UserPool const userpooldomain = new cognito.CfnUserPoolDomain(scope, 'UserPoolDomain', { @@ -121,13 +121,13 @@ export function setupCognitoForElasticSearch(scope: Construct, domainName: strin return cognitoAuthorizedRole; } -export function setupOpenSearchCognito(scope: Construct, domainName: string): +export function buildCognitoForSearchService(scope: Construct, domainName: string): [cognito.UserPool, cognito.UserPoolClient, cognito.CfnIdentityPool, iam.Role] { const userPool = buildUserPool(scope); const userPoolClient = buildUserPoolClient(scope, userPool); const identityPool = buildIdentityPool(scope, userPool, userPoolClient); - const cognitoAuthorizedRole: iam.Role = setupCognitoForElasticSearch(scope, domainName, { + const cognitoAuthorizedRole: iam.Role = setupCognitoForSearchService(scope, domainName, { userpool: userPool, identitypool: identityPool, userpoolclient: userPoolClient diff --git a/source/patterns/@aws-solutions-constructs/core/lib/elasticsearch-helper.ts b/source/patterns/@aws-solutions-constructs/core/lib/elasticsearch-helper.ts index 111413559..4adb770fe 100644 --- a/source/patterns/@aws-solutions-constructs/core/lib/elasticsearch-helper.ts +++ b/source/patterns/@aws-solutions-constructs/core/lib/elasticsearch-helper.ts @@ -13,6 +13,7 @@ import * as elasticsearch from 'aws-cdk-lib/aws-elasticsearch'; import { DefaultCfnDomainProps } from './elasticsearch-defaults'; +import { retrievePrivateSubnetIds } from './vpc-helper'; import { consolidateProps, addCfnSuppressRules } from './utils'; import * as iam from 'aws-cdk-lib/aws-iam'; import * as cdk from 'aws-cdk-lib'; @@ -218,25 +219,6 @@ export function buildElasticSearchCWAlarms(scope: Construct): cloudwatch.Alarm[] return alarms; } -function retrievePrivateSubnetIds(vpc: ec2.IVpc) { - let targetSubnetType; - - if (vpc.isolatedSubnets.length) { - targetSubnetType = ec2.SubnetType.PRIVATE_ISOLATED; - } else if (vpc.privateSubnets.length) { - targetSubnetType = ec2.SubnetType.PRIVATE_WITH_NAT; - } else { - throw new Error('Error - ElasticSearch Domains can only be deployed in Isolated or Private subnets'); - } - - const subnetSelector = { - onePerAz: true, - subnetType: targetSubnetType - }; - - return vpc.selectSubnets(subnetSelector).subnetIds; -} - function createClusterConfiguration(numberOfAzs?: number): elasticsearch.CfnDomain.ElasticsearchClusterConfigProperty { return { dedicatedMasterEnabled: true, diff --git a/source/patterns/@aws-solutions-constructs/core/lib/opensearch-defaults.ts b/source/patterns/@aws-solutions-constructs/core/lib/opensearch-defaults.ts index b27cc79df..906d89a9d 100644 --- a/source/patterns/@aws-solutions-constructs/core/lib/opensearch-defaults.ts +++ b/source/patterns/@aws-solutions-constructs/core/lib/opensearch-defaults.ts @@ -26,6 +26,9 @@ export function DefaultOpenSearchCfnDomainProps(domainName: string, cognitoConfi roleARNs.push(new iam.ArnPrincipal(props.serviceRoleARN)); } + // Features supported by engine version: + // https://docs.aws.amazon.com/opensearch-service/latest/developerguide/features-by-version.html + // https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_opensearchservice.CfnDomainProps.html return { domainName, engineVersion: 'OpenSearch_1.3', diff --git a/source/patterns/@aws-solutions-constructs/core/lib/opensearch-helper.ts b/source/patterns/@aws-solutions-constructs/core/lib/opensearch-helper.ts index 64c5ee8d7..22abdc1f6 100644 --- a/source/patterns/@aws-solutions-constructs/core/lib/opensearch-helper.ts +++ b/source/patterns/@aws-solutions-constructs/core/lib/opensearch-helper.ts @@ -13,6 +13,7 @@ import * as opensearch from 'aws-cdk-lib/aws-opensearchservice'; import { DefaultOpenSearchCfnDomainProps } from './opensearch-defaults'; +import { retrievePrivateSubnetIds } from './vpc-helper'; import { consolidateProps, addCfnSuppressRules } from './utils'; import * as iam from 'aws-cdk-lib/aws-iam'; import * as cdk from 'aws-cdk-lib'; @@ -40,7 +41,7 @@ export function buildOpenSearch(scope: Construct, props: BuildOpenSearchProps): const constructDrivenProps: any = {}; // Setup the IAM Role & policy for the OpenSearch Service to configure Cognito User pool and Identity pool - const cognitoKibanaConfigureRole = createKibanaCognitoRole(scope, props.userpool, props.identitypool, props.openSearchDomainName); + const cognitoDashboardConfigureRole = createDashboardCognitoRole(scope, props.userpool, props.identitypool, props.openSearchDomainName); if (props.vpc) { subnetIds = retrievePrivateSubnetIds(props.vpc); @@ -65,7 +66,7 @@ export function buildOpenSearch(scope: Construct, props: BuildOpenSearchProps): } } - const defaultCfnDomainProps = DefaultOpenSearchCfnDomainProps(props.openSearchDomainName, cognitoKibanaConfigureRole, props); + const defaultCfnDomainProps = DefaultOpenSearchCfnDomainProps(props.openSearchDomainName, cognitoDashboardConfigureRole, props); const finalCfnDomainProps = consolidateProps(defaultCfnDomainProps, props.clientDomainProps, constructDrivenProps); const opensearchDomain = new opensearch.CfnDomain(scope, `OpenSearchDomain`, finalCfnDomainProps); @@ -81,7 +82,7 @@ export function buildOpenSearch(scope: Construct, props: BuildOpenSearchProps): }, ]); - return [opensearchDomain, cognitoKibanaConfigureRole]; + return [opensearchDomain, cognitoDashboardConfigureRole]; } export function buildOpenSearchCWAlarms(scope: Construct): cloudwatch.Alarm[] { @@ -216,25 +217,6 @@ export function buildOpenSearchCWAlarms(scope: Construct): cloudwatch.Alarm[] { return alarms; } -function retrievePrivateSubnetIds(vpc: ec2.IVpc) { - let targetSubnetType; - - if (vpc.isolatedSubnets.length) { - targetSubnetType = ec2.SubnetType.PRIVATE_ISOLATED; - } else if (vpc.privateSubnets.length) { - targetSubnetType = ec2.SubnetType.PRIVATE_WITH_EGRESS; - } else { - throw new Error('Error - the OpenSearch Service domain can only be deployed in Isolated or Private subnets'); - } - - const subnetSelector = { - onePerAz: true, - subnetType: targetSubnetType - }; - - return vpc.selectSubnets(subnetSelector).subnetIds; -} - function createClusterConfiguration(numberOfAzs?: number): opensearch.CfnDomain.ClusterConfigProperty { return { dedicatedMasterEnabled: true, @@ -247,24 +229,24 @@ function createClusterConfiguration(numberOfAzs?: number): opensearch.CfnDomain. }; } -function createKibanaCognitoRole( +function createDashboardCognitoRole( scope: Construct, userPool: cognito.UserPool, identitypool: cognito.CfnIdentityPool, domainName: string ): iam.Role { // Setup the IAM Role & policy for the OpenSearch Service to configure Cognito User pool and Identity pool - const cognitoKibanaConfigureRole = new iam.Role( + const cognitoDashboardConfigureRole = new iam.Role( scope, - "CognitoKibanaConfigureRole", + "CognitoDashboardConfigureRole", { assumedBy: new iam.ServicePrincipal("es.amazonaws.com"), } ); - const cognitoKibanaConfigureRolePolicy = new iam.Policy( + const cognitoDashboardConfigureRolePolicy = new iam.Policy( scope, - "CognitoKibanaConfigureRolePolicy", + "CognitoDashboardConfigureRolePolicy", { statements: [ new iam.PolicyStatement({ @@ -295,12 +277,12 @@ function createKibanaCognitoRole( "iam:PassedToService": "cognito-identity.amazonaws.com", }, }, - resources: [cognitoKibanaConfigureRole.roleArn], + resources: [cognitoDashboardConfigureRole.roleArn], }), ], } ); - cognitoKibanaConfigureRolePolicy.attachToRole(cognitoKibanaConfigureRole); - return cognitoKibanaConfigureRole; + cognitoDashboardConfigureRolePolicy.attachToRole(cognitoDashboardConfigureRole); + return cognitoDashboardConfigureRole; } diff --git a/source/patterns/@aws-solutions-constructs/core/lib/vpc-helper.ts b/source/patterns/@aws-solutions-constructs/core/lib/vpc-helper.ts index 30c71612c..c8e1ff7c3 100644 --- a/source/patterns/@aws-solutions-constructs/core/lib/vpc-helper.ts +++ b/source/patterns/@aws-solutions-constructs/core/lib/vpc-helper.ts @@ -218,4 +218,23 @@ function AddGatewayEndpoint(vpc: ec2.IVpc, service: EndpointDefinition, interfac vpc.addGatewayEndpoint(interfaceTag, { service: service.endpointGatewayService as ec2.GatewayVpcEndpointAwsService, }); +} + +export function retrievePrivateSubnetIds(vpc: ec2.IVpc) { + let targetSubnetType; + + if (vpc.isolatedSubnets.length) { + targetSubnetType = ec2.SubnetType.PRIVATE_ISOLATED; + } else if (vpc.privateSubnets.length) { + targetSubnetType = ec2.SubnetType.PRIVATE_WITH_EGRESS; + } else { + throw new Error('Error - Subnet IDs must be Isolated or Private subnets'); + } + + const subnetSelector = { + onePerAz: true, + subnetType: targetSubnetType + }; + + return vpc.selectSubnets(subnetSelector).subnetIds; } \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/core/test/congnito-helper.test.ts b/source/patterns/@aws-solutions-constructs/core/test/congnito-helper.test.ts index c2e556404..bf7291273 100644 --- a/source/patterns/@aws-solutions-constructs/core/test/congnito-helper.test.ts +++ b/source/patterns/@aws-solutions-constructs/core/test/congnito-helper.test.ts @@ -89,7 +89,7 @@ test('Test override for buildIdentityPool', () => { }); }); -test('Test setupCognitoForElasticSearch', () => { +test('Test setupCognitoForSearchService', () => { const stack = new Stack(); const userpool = defaults.buildUserPool(stack); @@ -99,7 +99,7 @@ test('Test setupCognitoForElasticSearch', () => { }); const identitypool = defaults.buildIdentityPool(stack, userpool, userpoolclient); - defaults.setupCognitoForElasticSearch(stack, 'test-domain', { + defaults.setupCognitoForSearchService(stack, 'test-domain', { userpool, userpoolclient, identitypool diff --git a/source/patterns/@aws-solutions-constructs/core/test/elasticsearch-helper.test.ts b/source/patterns/@aws-solutions-constructs/core/test/elasticsearch-helper.test.ts index 3b5d09513..4dfb094a3 100644 --- a/source/patterns/@aws-solutions-constructs/core/test/elasticsearch-helper.test.ts +++ b/source/patterns/@aws-solutions-constructs/core/test/elasticsearch-helper.test.ts @@ -27,7 +27,7 @@ function deployES(stack: Stack, domainName: string, clientDomainProps?: elastics }); const identitypool = defaults.buildIdentityPool(stack, userpool, userpoolclient); - const cognitoAuthorizedRole = defaults.setupCognitoForElasticSearch(stack, 'test-domain', { + const cognitoAuthorizedRole = defaults.setupCognitoForSearchService(stack, 'test-domain', { userpool, userpoolclient, identitypool @@ -266,7 +266,7 @@ test('Test error thrown with no private subnet configurations', () => { deployES(stack, 'test-domain', {}, undefined, vpc); }; - expect(app).toThrowError('Error - ElasticSearch Domains can only be deployed in Isolated or Private subnets'); + expect(app).toThrowError('Error - Subnet IDs must be Isolated or Private subnets'); }); test('Test override ES version for buildElasticSearch', () => { diff --git a/source/patterns/@aws-solutions-constructs/core/test/opensearch-helper.test.ts b/source/patterns/@aws-solutions-constructs/core/test/opensearch-helper.test.ts index 08354bc00..411e8fd0b 100644 --- a/source/patterns/@aws-solutions-constructs/core/test/opensearch-helper.test.ts +++ b/source/patterns/@aws-solutions-constructs/core/test/opensearch-helper.test.ts @@ -27,7 +27,7 @@ function deployOpenSearch(stack: Stack, openSearchDomainName: string, clientDoma }); const identitypool = defaults.buildIdentityPool(stack, userpool, userpoolclient); - const cognitoAuthorizedRole = defaults.setupCognitoForElasticSearch(stack, 'test-domain', { + const cognitoAuthorizedRole = defaults.setupCognitoForSearchService(stack, 'test-domain', { userpool, userpoolclient, identitypool @@ -100,7 +100,7 @@ test('Test override SnapshotOptions for buildOpenSearch', () => { }, RoleArn: { "Fn::GetAtt": [ - "CognitoKibanaConfigureRole62CCE76A", + "CognitoDashboardConfigureRoleEC5F4809", "Arn" ] }, @@ -251,7 +251,7 @@ test('Test error thrown with no private subnet configurations', () => { deployOpenSearch(stack, 'test-domain', {}, undefined, vpc); }; - expect(app).toThrowError('Error - the OpenSearch Service domain can only be deployed in Isolated or Private subnets'); + expect(app).toThrowError('Error - Subnet IDs must be Isolated or Private subnets'); }); test('Test engine version override for buildOpenSearch', () => { @@ -302,7 +302,7 @@ test('Test engine version override for buildOpenSearch', () => { }, RoleArn: { "Fn::GetAtt": [ - "CognitoKibanaConfigureRole62CCE76A", + "CognitoDashboardConfigureRoleEC5F4809", "Arn" ] }, @@ -387,7 +387,7 @@ test('Test deployment with lambdaRoleARN', () => { }, RoleArn: { "Fn::GetAtt": [ - "CognitoKibanaConfigureRole62CCE76A", + "CognitoDashboardConfigureRoleEC5F4809", "Arn" ] }, From 64df18274ee05408f3cebe9c9f844b05f28fa68f Mon Sep 17 00:00:00 2001 From: mickychetta Date: Tue, 11 Oct 2022 17:37:39 +0000 Subject: [PATCH 9/9] added type safety to opensearch prop --- .../aws-lambda-opensearch/lib/index.ts | 16 +++++++----- .../test/lambda-opensearch.test.ts | 26 +++++-------------- .../core/lib/vpc-helper.ts | 2 +- .../core/test/elasticsearch-helper.test.ts | 2 +- .../core/test/opensearch-helper.test.ts | 2 +- 5 files changed, 19 insertions(+), 29 deletions(-) diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/lib/index.ts b/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/lib/index.ts index 40b12df4a..dd152dfbc 100644 --- a/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/lib/index.ts +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/lib/index.ts @@ -147,21 +147,23 @@ export class LambdaToOpenSearch extends Construct { [this.userPool, this.userPoolClient, this.identityPool, cognitoAuthorizedRole] = defaults.buildCognitoForSearchService(this, props.cognitoDomainName ?? props.openSearchDomainName); - const buildOpenSearchProps: any = { + let securityGroupIds; + + if (this.vpc) { + securityGroupIds = defaults.getLambdaVpcSecurityGroupIds(this.lambdaFunction); + } + + const buildOpenSearchProps: defaults.BuildOpenSearchProps = { userpool: this.userPool, identitypool: this.identityPool, cognitoAuthorizedRoleARN: cognitoAuthorizedRole.roleArn, serviceRoleARN: lambdaFunctionRoleARN, vpc: this.vpc, openSearchDomainName: props.openSearchDomainName, - clientDomainProps: props.openSearchDomainProps + clientDomainProps: props.openSearchDomainProps, + securityGroupIds }; - if (this.vpc) { - const securityGroupIds = defaults.getLambdaVpcSecurityGroupIds(this.lambdaFunction); - buildOpenSearchProps.securityGroupIds = securityGroupIds; - } - [this.openSearchDomain, this.openSearchRole] = defaults.buildOpenSearch(this, buildOpenSearchProps); if (props.createCloudWatchAlarms === undefined || props.createCloudWatchAlarms) { diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/test/lambda-opensearch.test.ts b/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/test/lambda-opensearch.test.ts index 09c6b995f..299bcc265 100644 --- a/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/test/lambda-opensearch.test.ts +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/test/lambda-opensearch.test.ts @@ -13,10 +13,10 @@ import { LambdaToOpenSearch, LambdaToOpenSearchProps } from "../lib"; import * as lambda from 'aws-cdk-lib/aws-lambda'; -import * as ec2 from 'aws-cdk-lib/aws-ec2'; import * as cdk from "aws-cdk-lib"; import '@aws-cdk/assert/jest'; import * as defaults from '@aws-solutions-constructs/core'; +import { getTestVpc } from "@aws-solutions-constructs/core"; function deployLambdaToOpenSearch(stack: cdk.Stack) { const props: LambdaToOpenSearchProps = { @@ -422,20 +422,8 @@ test('Test minimal deployment with an existing private VPC', () => { env: { account: "123456789012", region: 'us-east-1' }, }); - const vpc = new ec2.Vpc(stack, 'existing-private-vpc-test', { - natGateways: 1, - subnetConfiguration: [ - { - cidrMask: 24, - name: 'application', - subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS, - }, - { - cidrMask: 24, - name: "public", - subnetType: ec2.SubnetType.PUBLIC, - } - ] + const vpc = getTestVpc(stack, true, { + vpcName: "existing-private-vpc-test" }); const construct = new LambdaToOpenSearch(stack, 'test-lambda-opensearch', { @@ -448,7 +436,7 @@ test('Test minimal deployment with an existing private VPC', () => { Tags: [ { Key: "Name", - Value: "Default/existing-private-vpc-test" + Value: "existing-private-vpc-test" } ] }); @@ -457,13 +445,13 @@ test('Test minimal deployment with an existing private VPC', () => { VPCOptions: { SubnetIds: [ { - Ref: "existingprivatevpctestapplicationSubnet1Subnet1F7744F0" + Ref: "VpcPrivateSubnet1Subnet536B997A" }, { - Ref: "existingprivatevpctestapplicationSubnet2SubnetF7B713AD" + Ref: "VpcPrivateSubnet2Subnet3788AAA1" }, { - Ref: "existingprivatevpctestapplicationSubnet3SubnetA519E038" + Ref: "VpcPrivateSubnet3SubnetF258B56E" } ] } diff --git a/source/patterns/@aws-solutions-constructs/core/lib/vpc-helper.ts b/source/patterns/@aws-solutions-constructs/core/lib/vpc-helper.ts index c8e1ff7c3..6dd0fa463 100644 --- a/source/patterns/@aws-solutions-constructs/core/lib/vpc-helper.ts +++ b/source/patterns/@aws-solutions-constructs/core/lib/vpc-helper.ts @@ -228,7 +228,7 @@ export function retrievePrivateSubnetIds(vpc: ec2.IVpc) { } else if (vpc.privateSubnets.length) { targetSubnetType = ec2.SubnetType.PRIVATE_WITH_EGRESS; } else { - throw new Error('Error - Subnet IDs must be Isolated or Private subnets'); + throw new Error('Error - No isolated or private subnets available in VPC'); } const subnetSelector = { diff --git a/source/patterns/@aws-solutions-constructs/core/test/elasticsearch-helper.test.ts b/source/patterns/@aws-solutions-constructs/core/test/elasticsearch-helper.test.ts index 4dfb094a3..1966736a6 100644 --- a/source/patterns/@aws-solutions-constructs/core/test/elasticsearch-helper.test.ts +++ b/source/patterns/@aws-solutions-constructs/core/test/elasticsearch-helper.test.ts @@ -266,7 +266,7 @@ test('Test error thrown with no private subnet configurations', () => { deployES(stack, 'test-domain', {}, undefined, vpc); }; - expect(app).toThrowError('Error - Subnet IDs must be Isolated or Private subnets'); + expect(app).toThrowError('Error - No isolated or private subnets available in VPC'); }); test('Test override ES version for buildElasticSearch', () => { diff --git a/source/patterns/@aws-solutions-constructs/core/test/opensearch-helper.test.ts b/source/patterns/@aws-solutions-constructs/core/test/opensearch-helper.test.ts index 411e8fd0b..1d3dcee3b 100644 --- a/source/patterns/@aws-solutions-constructs/core/test/opensearch-helper.test.ts +++ b/source/patterns/@aws-solutions-constructs/core/test/opensearch-helper.test.ts @@ -251,7 +251,7 @@ test('Test error thrown with no private subnet configurations', () => { deployOpenSearch(stack, 'test-domain', {}, undefined, vpc); }; - expect(app).toThrowError('Error - Subnet IDs must be Isolated or Private subnets'); + expect(app).toThrowError('Error - No isolated or private subnets available in VPC'); }); test('Test engine version override for buildOpenSearch', () => {