TL;DR
π§ Learn how to deploy a Lambda with a static IP (for whitelisting concerns)
β‘ Perform NodeJS SFTP operations using this Lambda.
Your Serverless application might need to connect to a partner server that requires IP whitelisting.
We will use the Serverless Framework to deploy our Lambda function and the Typescript AWS CDK to create the infrastructure needed to give our Lambda a static IP π.
If you are not familiar with these libraries, I recommend you check out this Ultimate Serverless DevX: Serverless Framework x CDK blog post to learn how to conciliate the two. π€
Moreover, we will use the NodeJS ssh2-promise package to perform SFTP operations in our Lambda function. π½
This is what we will build π·:
Let's get to work πͺπ¨!
1οΈβ£ Create a VPC hosted Lambda function with a static IP
βοΈ VPC, NAT Gateway and Elastic IP
First, we need to create a VPC with a public subnet and a private subnet. The private subnet will host our Lambda function and the public subnet will host our NAT Gateway.
const vpc = new Vpc(stack, 'Vpc', {
vpcName: 'vpc',
natGateways: 1, // π Automatically creates an Elastic IP
maxAzs: 1, // π Use more if you need high availability
subnetConfiguration: [
{
name: 'private-subnet-1',
subnetType: SubnetType.PRIVATE_WITH_NAT,
cidrMask: 24,
},
{
name: 'public-subnet-1',
subnetType: SubnetType.PUBLIC,
cidrMask: 24,
},
],
});
πΈ Pricing Disclaimer
This architecture will cost some money. The NAT Gateway runs on a EC2 instance which will cost you around 30$/month. This price is multiplied by the number of AZs you use.
I also recommend that you use CloudFormation Outputs to store the IDs of the resources you will need later on. We want to slap that β¨clean IaC codeβ¨ in our PR ποΈ.
export const vpcSecurityGroupOutputId = 'SgOutputId';
export const vpcPrivateSubnetOutputId = 'VpcPvSubOutputId';
...
const privateSubnets = vpc.selectSubnets(
{ subnetType: SubnetType.PRIVATE_WITH_NAT }
);
export const [privateSubnetId1] = privateSubnets.subnetIds;
// π Outputing the IDs of the resources in case another stack needs them
new CfnOutput(stack, vpcSecurityGroupOutputId, {
value: vpc.vpcDefaultSecurityGroup,
});
new CfnOutput(stack, vpcPrivateSubnet1OutputId, {
value: privateSubnetId1,
});
β‘ Deploy the Lambda function
Now that we have our VPC, we can deploy our Lambda function, using the Serverless Framework.
export const staticIpLambda = {
timeout: 15,
handler: getHandlerPath(__dirname),
vpc: {
securityGroupIds: [ stack.resolve(vpc.vpcDefaultSecurityGroup) ],
subnetIds: [ stack.resolve(privateSubnetId1) ],
},
};
You can also check out this Serverless Framework x CDK article to learn how to leverage the stack.resolve()
method π§βπ.
And bam! π Our Lambda function is deployed in our private subnet.
All the outbound traffic from our Lambda function will now go through the NAT Gateway, which will use the Elastic IP we created earlier. π
You can retrieve the Elastic IP of your NAT Gateway using the AWS Console.
Go to the VPC service and click Elastic IPs. You should see the Elastic IP created by the CDK.
Now just message your partner and ask them to whitelist this IP. π©
2οΈβ£ Perform SFTP operations in your Lambda function
β This part is actually optional.
We will cover a very specific SFTP use case and a problem I encountered while using thessh2-promise
and theserverless-esbuild
plugin.
Now let's talk about a very specific use case: performing SFTP operations in your Lambda function.
I wanted to share with you this use case because I came across weird issues with the ssh2-promise
package not being correctly bundled into my Lambda .zip code
. π€―
π SSH Key
If your SFTP partner requires a SSH Key, I recommend you to store the key in AWS Secrets Manager. You can then retrieve it in your Lambda function using Middy Secrets Manager middleware. It will provide the key as a string in your Lambda function context. π€
π½ Store the file you want to send in your Lambda's /tmp
folder
The ssh2-promise
package will need file system access to send your file. One way to achieve that in a Lambda function context is to leverage the /tmp
folder.
This folder is writable and will be deleted when your Lambda function is terminated.
π§ Warning
The/tmp
folder is not persistent. If you want to keep track of the files you've sent you should also use S3. π¦
Also be aware that the/tmp
folder is shared between successive Lambda invocations in the same execution environment. Use a unique file name to avoid any bugs π.
This is what the final code looks like with the ssh2-promise
package:
fs.writeFileSync('/tmp/myFile.txt', 'Hello World!');
const sshPrivateKey = context["SSH_PRIVATE_KEY"]
const ssh = new SSH2Promise({
host: 'sftpIpAddress',
username: 'sftpUsername',
privateKey: sshPrivateKey,
port: "SFTP_PORT",
});
await ssh.connect();
const sftp = ssh.sftp();
await sftp.fastPut('/tmp/myFile.txt', "distant_name.txt");
await ssh.close();
The catch π£
If like me you are using the serverless-esbuild
plugin to bundle your Lambda function, you might encounter some weird issues.
The ssh2-promise
package is not correctly bundled into your Lambda function. π€―
To solve this issue, I first patched the serverless-esbuild
plugin to allow yarn3 module bundling exclusion. I want the ssh2-promise
package NOT to be bundled by esbuild.
The patch is available here as a github gist
Then, I had to add the ssh2-promise
package to the externals
section of my esbuild
config.
// serverless.ts
const serverlessConfiguration = {
..., // π Your other serverless config
custom: {
esbuild: { ...esbuildConfig, external: ['ssh2-promise'] },
},
};
Your Lambda function should now be able to perform SFTP operations. π
Thanks for reading! π
If you have any questions or feedback, feel free to reach out to me on Twitter or ask a question in the comments below. π₯Έ
Also check out my Dev.to for more articles about AWS, Serverless and Cloud Development. π
Top comments (9)
great post!
Thanks ! π₯
Awesome guide!...wouldn't mind a repo to base off of.
CDK code? No thanks, I don't want to be a javescript programmer
CDK supports multiple languages, TS is an implementation detail here π
Terraform code all the time, to create a vpc I need to write code like If I need to write an application
that is just a matter of personal taste.
If I am writing already typescript for my lambda (or python) I find it was easier to write my IaC with TS or python. why the heck the freaking yaml..
Thanks for the post !
Hi, have you this full example in Github?