How to run npm ci from Terraform

Install Node dependencies during deployment in an optimized way

Author's image
Tamás Sallai
1 min

NPM dependencies in Lambda

You can define Lambda code inline in an archive_file data source, but that works only for simple functions without any dependencies. For anything more serious, you'll need a package.json and npm to install third-party libraries.

But then, before you upload the code to AWS you need to run npm ci. Terraform's archive_file does little to help here: it's a dumb construct that zips anything that happens to be in the source directory. Which, if you forgot to install the dependencies breaks the function.

How to make Terraform run npm ci automatically?

Running external programs

Terraform supports the external data source that provides support for arbitrary commands. This allows complex builds to run as part of the deploy process.

But npm ci is needed only occassionally, so if it's run every time you run terraform apply or terraform plan, it slows down all deployments considerably.

Fortunately, both the node_modules and the package-lock.json have a modification timestamp. npm ci is only needed if the time of the node_modules is earlier than the time of package-lock.json. And this is exactly what make supports.

Create a makefile that has a node_modules target that depends on package-lock.json:

# Makefile

node_modules: package-lock.json
	npm ci

In the Terraform config, run make node_modules:

# main.tf

data "external" "build" {
	program = ["bash", "-c", <<EOT
(make node_modules) >&2 && echo "{\"dest\": \".\"}"
EOT
	]
	working_dir = "${path.module}/src"
}

data "archive_file" "lambda_zip" {
	type        = "zip"
	output_path = "/tmp/lambda-${random_id.id.hex}.zip"
	source_dir  = "${data.external.build.working_dir}/${data.external.build.result.dest}"
}

resource "aws_lambda_function" "lambda" {
	# ...
	filename         = data.archive_file.lambda_zip.output_path
	source_code_hash = data.archive_file.lambda_zip.output_base64sha256
}

Note the result value of the external build ({dest: "."}) and the source_dir argument of the archive_file. This defines an implicit dependency between the two, so the dependencies are guaranteed to be installed when the archive_file zips the folder.

March 15, 2022
In this article