Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Typescript example of AWS AppConfig hosted configuration with lambda using AppConfig Lambda extension integration #361

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ $ cdk destroy
| [custom-logical-names](https://github.com/aws-samples/aws-cdk-examples/tree/master/typescript/custom-logical-names/) | Example of how to override logical name allocation |
| [fargate-service-with-efs](https://github.com/aws-samples/aws-cdk-examples/tree/master/typescript/ecs/fargate-service-with-efs/) | Starting a container fronted by an application load balancer on Fargate with an EFS Mount |
| [http-proxy-apigateway](https://github.com/aws-samples/aws-cdk-examples/tree/master/typescript/http-proxy-apigateway/) | Use ApiGateway to set up a http proxy |
| [appconfig-hosted-configuration-lambda-extension]() | Example of AWS AppConfig hosted configuration with consuming Lambda using AppConfig Lambda extension integration |


## Java examples <a name="Java"></a>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# AWS AppConfig Hosted Configuration with Lambda Extension
<!--BEGIN STABILITY BANNER-->
---

![Stability: Stable](https://img.shields.io/badge/stability-Stable-success.svg?style=for-the-badge)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This actually isn't considered a "stable" module since only the L1 containing Cfn resources exists. You can see the banner here and explanation of reasoning here


> **This is a stable example. It should successfully build out of the box**
>
> This examples does is built on Construct Libraries marked "Stable" and does not have any infrastructure prerequisites to build.

---
<!--END STABILITY BANNER-->

This an example of AppConfig fature toggle use case with a hosted configuration and a consuming Lambda using AppConfig Lambda extension integration.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would be a great place to include a brief description of why this pattern might be useful.
eg. "Deploying an AppConfig resource can allow for easier deployments to multiple environments from development to production and allow for easier changing of necessary configurations depending on environment ...."


Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An explanation of the purpose of the lambda would also be important here. At the moment it is very difficult to decipher (and I imagine even more so for someone with less context than me).

## Build

To build this app, you need to be in this example's root folder. Then run the following:

```bash
npm install -g aws-cdk
npm install
npm run build
```

This will install the necessary CDK, then this example's dependencies, and then build your TypeScript files and your CloudFormation template.

## Deploy

Run `cdk deploy`. This will deploy / redeploy your Stack to your AWS Account.

After the deployment you will see the API's URL, which represents the url you can then use.

## The Component Structure

The whole component contains:

- An AppConfig Application, Environment, Hosted Configuration Profile and Deployment Strategy.
- Lambda pointing to `src/lambda-handler.ts`, containing code for __consuming__ AppConfig configuration data.

## CDK Toolkit

The [`cdk.json`](./cdk.json) file in the root of this repository includes
instructions for the CDK toolkit on how to execute this program.

After building your TypeScript code, you will be able to run the CDK toolkits commands as usual:

$ cdk ls
<list all stacks in this program>

$ cdk synth
<generates and outputs cloudformation template>

$ cdk deploy
<deploys stack to your account>

$ cdk diff
<shows diff against deployed stack>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"app": "node index"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import * as cdk from '@aws-cdk/core';
import { CfnApplication, CfnEnvironment, CfnConfigurationProfile, CfnHostedConfigurationVersion, CfnDeploymentStrategy, CfnDeployment } from '@aws-cdk/aws-appconfig';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should space long imports on separate lines (or import * and use an accessor

Suggested change
import { CfnApplication, CfnEnvironment, CfnConfigurationProfile, CfnHostedConfigurationVersion, CfnDeploymentStrategy, CfnDeployment } from '@aws-cdk/aws-appconfig';
import {
CfnApplication,
CfnEnvironment,
CfnConfigurationProfile,
CfnHostedConfigurationVersion,
CfnDeploymentStrategy,
CfnDeployment
} from '@aws-cdk/aws-appconfig';

import { Function, AssetCode, Runtime, LayerVersion } from '@aws-cdk/aws-lambda';
import { Effect, PolicyStatement } from '@aws-cdk/aws-iam';

export class AppConfigHostedConfigurationStack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);

const application = new CfnApplication(this,'AppConfigApplication', {
name: 'AppConfigSampleApplication',
description: 'Sample AppConfig Application using CDK'
});

const environment = new CfnEnvironment(this, 'LambdaDevelopmentEnvironment', {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be useful here to include both a dev and prod environment with appropriate configurations for each. This would show the real power/reason to use AppConfig over a standard deployment strategy.

applicationId: application.ref,
name: 'AppConfigSampleLambdaDevelopmentEnvironment',
description: 'Sample AppConfig Development environment for Lambda implementation'
});

const configurationProfile = new CfnConfigurationProfile(this, 'ConfigurationProfile', {
applicationId: application.ref,
name: 'AppConfigSampleConfigurationProfile',
locationUri: 'hosted',
description: 'Sample AppConfig configuration profile'
});

const hostedConfigurationProfile = new CfnHostedConfigurationVersion(this, 'HostedConfigurationProfile', {
applicationId: application.ref,
configurationProfileId: configurationProfile.ref,
contentType: 'application/json',
content: '{\"boolEnableLimitResults\": true, \"intResultLimit\":5}',
latestVersionNumber: 1
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is optional to prevent overwriting previous config versions, but since there is only one version being deployed, this should be skipped

});

hostedConfigurationProfile.addMetadata('description', 'Sample AppConfig hosted configuration profile content');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this not provided in props?


const deploymentStrategy = new CfnDeploymentStrategy(this, 'DeploymentStrategy', {
name: 'Custom.AllAtOnce',
deploymentDurationInMinutes: 0,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My experience with AppConfig is very limited, but should this not be greater than 0?

growthFactor: 100,
finalBakeTimeInMinutes: 0,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same question as above?

replicateTo: 'NONE',
growthType: 'LINEAR',
description: 'Sample AppConfig deployment strategy - All at once deployment (i.e., immediate)'
});

const deployment = new CfnDeployment(this, 'Deployment', {
applicationId: application.ref,
configurationProfileId: configurationProfile.ref,
configurationVersion: '1',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another magic number. This should be:

Suggested change
configurationVersion: '1',
configurationVersion: constedConfigurationProfile.latestVersionNumber,

Copy link

@unclenorton unclenorton Dec 9, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@NGL321 not exactly intuitive, but looks like hostedConfigurationProfile.ref should be used instead. latestVersionNumber is undefined here.

deploymentStrategyId: deploymentStrategy.ref,
environmentId: environment.ref,
});
Comment on lines +10 to +54
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider leaving an inline comment to indicate that these are essential components to an AppConfig configuration, and explain what each does along the way.

For inspiration, take a look at the Cloudformation description


deployment.addDependsOn(hostedConfigurationProfile);

deployment.addMetadata('description', 'Sample AppConfig initial deployment');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, provide in props?


const sampleAppConfigLambda = new Function(this, 'sampleAppConfigLambda', {
code: new AssetCode('src'),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By standards this should be:

Suggested change
code: new AssetCode('src'),
code: Code.fromAsset('src'),

This also requires adjusting imports

functionName: 'SampleAppConfigLambda',
handler: 'lambda-handler.handler',
runtime: Runtime.NODEJS_12_X,
environment: {
AWS_APPCONFIG_EXTENSION_HTTP_PORT: '2772',
AWS_APPCONFIG_EXTENSION_POLL_INTERVAL_SECONDS: '45',
AWS_APPCONFIG_EXTENSION_POLL_TIMEOUT_MILLIS: '3000'
},
layers: [
LayerVersion.fromLayerVersionArn(this, 'AppConfigLambdaExtension', 'arn:aws:lambda:us-east-1:027255383542:layer:AWS-AppConfig-Extension:1')
]
});

sampleAppConfigLambda.addToRolePolicy(
new PolicyStatement({
resources: [
`arn:aws:appconfig:${this.region}:${this.account}:application/${application.ref}*`
],
actions: ['appconfig:GetConfiguration'],
effect: Effect.ALLOW
})
);
}
}

const app = new cdk.App();
new AppConfigHostedConfigurationStack(app, 'AppConfigHostedConfigurationStack');
app.synth();
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"name": "appconfig-hosted-configuration",
"version": "1.0.0",
"description": "Use of AWS AppConfig with a hosted configuration",
"private": true,
"scripts": {
"build": "tsc",
"watch": "tsc -w",
"cdk": "cdk"
},
"author": {
"name": "Amazon Web Services",
"url": "https://aws.amazon.com",
"organization": true
},
"license": "Apache-2.0",
"devDependencies": {
"@types/node": "10.17.27",
"typescript": "~3.9.7"
},
"dependencies": {
"@aws-cdk/aws-appconfig": "*",
"@aws-cdk/aws-iam": "*",
"@aws-cdk/aws-lambda": "*",
"@aws-cdk/core": "*"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import * as http from 'http';


export const handler = async (): Promise<any> => {
// retrieve AppConfig configuration data from Lambda extension
const res: any = await new Promise((resolve) => {
http.get(
`http://localhost:2772/applications/AppConfigSampleApplication/environments/AppConfigSampleLambdaDevelopmentEnvironment/configurations/AppConfigSampleConfigurationProfile`,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe I'm missing something, but I dont see where this URL is coming from

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this only works with Lambda/ECS/Fargate workloads, normally you would acquire these config values via the AWS SDK: https://docs.aws.amazon.com/appconfig/latest/userguide/appconfig-integration-lambda-extensions.html#appconfig-integration-lambda-extensions-enabling

There's also this blog post that references the localhost URL proxy: https://aws.amazon.com/blogs/mt/introducing-aws-appconfig-lambda-extension-deploying-application-configuration-serverless/

resolve
);
});

let configData: any = await new Promise((resolve, reject) => {
let data = '';
res.on('data', (chunk: any) => data += chunk);
res.on('error', (err: any) => {
console.log(err);
reject(err);
});
res.on('end', () => resolve(data));
});

let result: {name: String}[] = getServices();
const parsedConfigData = JSON.parse(configData);

// implement feature toggle that filters results using configuration data
if ( (parsedConfigData.boolEnableLimitResults) && parsedConfigData.intResultLimit ) {
result = result.splice(0, parsedConfigData.intResultLimit);
}

return result;
}

const getServices = () => {
return [
{
name: 'AWS AppConfig'
},
{
name: 'Amazon SageMaker Studio'
},
{
name: 'Amazon Kendra'
},
{
name: 'Amazon CodeGuru'
},
{
name: 'Amazon Fraud Detector'
},
{
name: 'Amazon EKS on AWS Fargate'
},
{
name: 'AWS Outposts'
},
{
name: 'AWS Wavelength'
},
{
name: 'AWS Transit Gateway'
},
{
name: 'Amazon Detective'
}
]
Comment on lines +35 to +66
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can make this a little more condensed while still obeying the eslint rules.

Suggested change
return [
{
name: 'AWS AppConfig'
},
{
name: 'Amazon SageMaker Studio'
},
{
name: 'Amazon Kendra'
},
{
name: 'Amazon CodeGuru'
},
{
name: 'Amazon Fraud Detector'
},
{
name: 'Amazon EKS on AWS Fargate'
},
{
name: 'AWS Outposts'
},
{
name: 'AWS Wavelength'
},
{
name: 'AWS Transit Gateway'
},
{
name: 'Amazon Detective'
}
]
return [
{ name: 'AWS AppConfig' },
{ name: 'Amazon SageMaker Studio' },
{ name: 'Amazon Kendra' },
{ name: 'Amazon CodeGuru' },
{ name: 'Amazon Fraud Detector' },
{ name: 'Amazon EKS on AWS Fargate' },
{ name: 'AWS Outposts' },
{ name: 'AWS Wavelength' },
{ name: 'AWS Transit Gateway' },
{ name: 'Amazon Detective' }
]

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"compilerOptions": {
"target":"ES2018",
"module": "commonjs",
"lib": ["es2016", "es2017.object", "es2017.string"],
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"noImplicitThis": true,
"alwaysStrict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": false,
"inlineSourceMap": true,
"inlineSources": true,
"experimentalDecorators": true,
"strictPropertyInitialization":false
}
}