<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>npm on Barney Parker</title><link>https://barneyparker.com/tags/npm/</link><description>Recent content in npm on Barney Parker</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><copyright>This work is licensed under a Creative Commons Attribution-NonCommercial 4.0 International License.</copyright><lastBuildDate>Tue, 23 Jun 2020 14:29:42 +0000</lastBuildDate><atom:link href="https://barneyparker.com/tags/npm/index.xml" rel="self" type="application/rss+xml"/><item><title>Installing NPM Dependencies with Terraform</title><link>https://barneyparker.com/posts/installing-npm-dependencies-with-terraform/</link><pubDate>Tue, 23 Jun 2020 14:29:42 +0000</pubDate><guid>https://barneyparker.com/posts/installing-npm-dependencies-with-terraform/</guid><description>&lt;p>Terraform has the capability to create a zip file containing Function code, and for simpler functions that&amp;rsquo;s fine. For more complex functions it quickly becomes necessary to install dependencies to make avoid the need to write every line of code ourselves.&lt;/p>
&lt;p>The problem with Terraform is that it only wants to do things that have a Provider resource. &lt;code>npm install&lt;/code>ing dependencies needs to be done outside of this, and while we &lt;em>could&lt;/em> manually there is no way to guarantee we will remember to do it every time we apply.&lt;/p>
&lt;p>In order to keep this inside out Terraform workflow we need to abuse some resources a little.&lt;/p>
&lt;h2 id="triggering-dependency-installation">Triggering Dependency Installation&lt;/h2>
&lt;p>To convince Terraform to install our dependencies, we need to know&lt;/p>
&lt;ul>
&lt;li>Has &lt;code>package.json&lt;/code> changed?&lt;/li>
&lt;li>Has &lt;code>package-lock.json&lt;/code> changed?&lt;/li>
&lt;li>Do we have all the files we expected?&lt;/li>
&lt;/ul>
&lt;p>We also need to store something in the state file that we can compare to in the future.&lt;/p>
&lt;div class="highlight">&lt;pre class="chroma">&lt;code class="language-hcl" data-lang="hcl">&lt;span class="k">resource&lt;/span> &lt;span class="s2">&amp;#34;null_resource&amp;#34; &amp;#34;lambda_dependencies&amp;#34;&lt;/span> {
&lt;span class="k">provisioner&lt;/span> &lt;span class="s2">&amp;#34;local-exec&amp;#34;&lt;/span> {
&lt;span class="n"> command&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;cd ${path.module}/src &amp;amp;&amp;amp; npm install&amp;#34;&lt;/span>
}
&lt;span class="n"> triggers&lt;/span> &lt;span class="o">=&lt;/span> {
&lt;span class="n"> index&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">sha256&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">file&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;${path.module}/src/index.js&amp;#34;&lt;/span>&lt;span class="p">))&lt;/span>
&lt;span class="n"> package&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">sha256&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">file&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;${path.module}/src/package.json&amp;#34;&lt;/span>&lt;span class="p">))&lt;/span>
&lt;span class="n"> lock&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">sha256&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">file&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;${path.module}/src/package-lock.json&amp;#34;&lt;/span>&lt;span class="p">))&lt;/span>
&lt;span class="n"> node&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">sha256&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">join&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;&amp;#34;,fileset(path.module, &amp;#34;src/**/*.js&amp;#34;&lt;/span>&lt;span class="p">)))&lt;/span>
}
}
&lt;/code>&lt;/pre>&lt;/div>&lt;p>a &lt;code>null_resource&lt;/code> doesn&amp;rsquo;t do anything in itself, but it acts as a container that we can use to create one resource based on a set of trigger values.&lt;/p>
&lt;p>&lt;code>local-exec&lt;/code> allows us to run a system command, which in our case is to move in to the Lambda Function source directory and execute &lt;code>npm install&lt;/code>.&lt;/p>
&lt;p>We can set &lt;code>triggers&lt;/code> based on our requirement, i.e. has &lt;code>index.js&lt;/code>, &lt;code>package.json&lt;/code> or &lt;code>package-lock.json&lt;/code> changed by checking their sha256 signatures. We can also get a list of every .js file in the source directory, join them in to a single string and then get the sha256 of that string.&lt;/p>
&lt;p>These trigger values will be stored in the state file, so even if we run this on another machine we&amp;rsquo;re going to be able to tell if anything doesn&amp;rsquo;t match between the previous deployment and this one. If something doesn&amp;rsquo;t match, then the local-exec command will be executed, and our dependencies installed!&lt;/p>
&lt;h2 id="building-the-bundle">Building the Bundle&lt;/h2>
&lt;p>So far so good, but we need to enforce Terraform ordering in the dependency graph. If we just add an &lt;code>archive_file&lt;/code> data resource there&amp;rsquo;s no guarantee it will wait for the dependencies to be installed before creating the bundle.&lt;/p>
&lt;p>To fix this we add an intermediate resource called a &lt;code>null_data_source&lt;/code>. These resources take a bunch of inputs which can be computed form the outputs of other resources or variables. Until all of the inputs are satisfied Terraform wont allow anything depending on the &lt;code>null_data_source&lt;/code> to be created or modified.&lt;/p>
&lt;div class="highlight">&lt;pre class="chroma">&lt;code class="language-hcl" data-lang="hcl">&lt;span class="k">data&lt;/span> &lt;span class="s2">&amp;#34;null_data_source&amp;#34; &amp;#34;wait_for_lambda_exporter&amp;#34;&lt;/span> {
&lt;span class="n"> inputs&lt;/span> &lt;span class="o">=&lt;/span> {
&lt;span class="n"> lambda_dependency_id&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;${null_resource.lambda_dependencies.id}&amp;#34;&lt;/span>
&lt;span class="n"> source_dir&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;${path.module}/src/&amp;#34;&lt;/span>
}
}
&lt;/code>&lt;/pre>&lt;/div>&lt;p>The &lt;code>lambda_dependency_id&lt;/code> input wont be known to Terraform until the previous &lt;code>null_resource&lt;/code> has completed - which means our &lt;code>npm install&lt;/code> has either been completed, or nothing had changed and it wasn&amp;rsquo;t required.&lt;/p>
&lt;p>The &lt;code>source_dir&lt;/code> input can be pre-computed by Terraform, and we will use this value to tell the &lt;code>archive_file&lt;/code> what to add to the bundle.&lt;/p>
&lt;p>Finally we create our &lt;code>archive_file&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre class="chroma">&lt;code class="language-hcl" data-lang="hcl">&lt;span class="k">data&lt;/span> &lt;span class="s2">&amp;#34;archive_file&amp;#34; &amp;#34;lambda&amp;#34;&lt;/span> {
&lt;span class="n"> output_path&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;${path.module}/lambda-bundle.zip&amp;#34;&lt;/span>
&lt;span class="n"> source_dir&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;${data.null_data_source.wait_for_lambda_exporter.outputs[&amp;#34;source_dir&amp;#34;]}&amp;#34;&lt;/span>
&lt;span class="n"> type&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;zip&amp;#34;&lt;/span>
}
&lt;/code>&lt;/pre>&lt;/div>&lt;p>So now we&amp;rsquo;re creating our &lt;code>archive_file.lambda&lt;/code> from the path given to us by &lt;code>null_data_source.wait_for_lambda_exporter&lt;/code>, which wont give us an answer until &lt;code>null_resource.lambda_dependencies&lt;/code> is satisfied, which ensures the correct dependency ordering is maintained!&lt;/p>
&lt;h2 id="less-than-ideal">Less than ideal&lt;/h2>
&lt;p>There is one minor issue with this method. On the first run there is no node modules, so the null_resource.lambda_dependencies.trigger.node value gets an initial sha256 value, and that causes npm install to be executed. This will then change the sha256 that would be computed to the src directory, and a second run would also create a new bundle. subsequent runs wont do this, but in an ideal world a single run should be all that is needed to converge our required to actual states.&lt;/p>
&lt;p>Its certainly not perfect, but if you can tolerate this one caveat, its a relatively simple way to include node dependencies without having to do anything more elaborate than a normal deployment.&lt;/p></description></item></channel></rss>