Repository: bustlelabs/shep
Pull request #193 (2017-02-07 02:43:09)

add config var support
- [x] Add unset
- [x] Test!!!! 
  - [x] List
  - [x] Remove
  - [x] Set
- [x] Fix broken tests
- [x] Ensure deploy does NOT overwrite old env vars (deploy should upload code, then lambda.json config w/ old env vars merged)
- [x] Cleanup
  - [x] Figure out `load.env` with no `environments` dir
- [x] Readme
  - [x] `shep config *`
  - [x] Migration guide start :)


Patch:
diff --git a/README.md b/README.md
index 29297a9..d2903c1 100644
--- a/README.md
+++ b/README.md
@@ -196,6 +196,48 @@ Options:
 Examples:
   shep logs                 Launch an interactive CLI
   shep logs production foo  Shows logs for the `foo` function in the production environment
+
+```
+#### `shep config list`
+```
+shep config list [env] [functionName]
+
+Options:
+  --version  Show version number                                                                               [boolean]
+  --help     Show help                                                                                         [boolean]
+
+Examples:
+  shep config list            Launches an interactive CLI to choose env and functionName
+  shep config list beta functionName   Lists the environment variables and values for foo with alias beta
+```
+
+```
+#### `shep config set`
+```
+shep config set [env] [vars...]
+
+Options:
+  --version  Show version number                                                                               [boolean]
+  --help     Show help                                                                                         [boolean]
+
+Examples:
+  shep config set beta FOO=BAR BAR=FOO Adds these environment variables and values to all functions and aliases them to beta
+```
+
+```
+#### `shep config remove`
+```
+shep config remove [env] [vars...]
+
+Options:
+  --version  Show version number                                                                               [boolean]
+  --help     Show help                                                                                         [boolean]
+
+Examples:
+  shep config remove beta FOO  Removes the environment variable FOO from beta aliased functions
+  shep config remove production FOO BAR Removes FOO and BAR environment variables from production functions
+```
+
 ```
 #### `shep generate function`
 ```
@@ -209,6 +251,7 @@ Options:
 Examples:
   shep generate function      Launch an interactive CLI
   shep generate function foo  Genereate a new functon called "foo"
+
 ```
 #### `shep generate endpoint`
 ```
diff --git a/migration.md b/migration.md
index 11db707..cbc422a 100644
--- a/migration.md
+++ b/migration.md
@@ -79,3 +79,7 @@ json, and it should live inside the `/environments` directory
 - Environment vars are now stored on `process.env`, like `process.env.ENV`. 
 - Capitalize all config keys inside environment files
 - `shep run` accepts custom process.env key value pairs, e.g., `DB_TABLE=custom shep run ${functionName}`
+
+## Migrating from shep 3.X to shep 4.0
+
+- Shep 4.0 now handles environment variables in AWS. See the docs for `shep config` to add, remove, and list environment variables for your functions.
diff --git a/package.json b/package.json
index 88c68cb..73cae21 100644
--- a/package.json
+++ b/package.json
@@ -42,6 +42,7 @@
     "bluebird": "^3.4.7",
     "chalk": "^1.1.3",
     "cliui": "^3.2.0",
+    "dotenv": "^4.0.0",
     "execa": "^0.6.0",
     "fs-extra-promise": "^0.4.1",
     "inquirer": "^2.0.0",
diff --git a/src/commands/config.js b/src/commands/config.js
new file mode 100644
index 0000000..96b47c2
--- /dev/null
+++ b/src/commands/config.js
@@ -0,0 +1,8 @@
+export const command = 'config'
+export const desc = 'Run `shep config --help` for additional information'
+export function builder (yargs) {
+  return yargs
+  .commandDir('./config')
+  .demand(1)
+  .strict()
+}
diff --git a/src/commands/config/list.js b/src/commands/config/list.js
new file mode 100644
index 0000000..de37404
--- /dev/null
+++ b/src/commands/config/list.js
@@ -0,0 +1,48 @@
+import inquirer from 'inquirer'
+import configList from '../../config-list'
+import * as load from '../../util/load'
+import merge from 'lodash.merge'
+
+export const command = 'list [env] [function]'
+export const desc = 'List environment variables on AWS for an alias'
+export function builder (yargs) {
+  return yargs
+  .pkgConf('shep', process.cwd())
+  .example('shep config beta foo', 'List environment variables for function "foo" beta alias')
+}
+
+export async function handler (opts) {
+  const envs = await load.envs()
+  let questions
+
+  if (envs && envs.length > 0) {
+    questions = [
+      {
+        name: 'env',
+        message: 'Environment',
+        type: 'list',
+        choices: () => envs
+      },
+      {
+        name: 'function',
+        message: 'Function',
+        type: 'list',
+        choices: () => load.funcs()
+      }
+    ]
+  } else {
+    if (!opts.env) { console.log('no API found, cannot load available aliases') }
+    questions = [
+      {
+        name: 'function',
+        message: 'Function',
+        type: 'list',
+        choices: () => load.funcs()
+      }
+    ]
+  }
+
+  inquirer.prompt(questions.filter((q) => !opts[q.name]))
+  .then((inputs) => merge({}, inputs, opts))
+  .then(configList)
+}
diff --git a/src/commands/config/remove.js b/src/commands/config/remove.js
new file mode 100644
index 0000000..f1e4e79
--- /dev/null
+++ b/src/commands/config/remove.js
@@ -0,0 +1,35 @@
+import inquirer from 'inquirer'
+import configRemove from '../../config-remove'
+import * as load from '../../util/load'
+import merge from 'lodash.merge'
+
+export const command = 'remove <env> <vars...>'
+export const desc = 'Remove environment variables for alias on AWS'
+export function builder (yargs) {
+  return yargs
+  .pkgConf('shep', process.cwd())
+  .example('shep config remove beta NEW_VARIABLE', 'Removes NEW_VARIABLE from all functions with beta alias')
+}
+
+export async function handler (opts) {
+  const envs = await load.envs()
+
+  if (envs && envs.length > 0) {
+    const questions = [
+      {
+        name: 'env',
+        message: 'Environment',
+        type: 'list',
+        choices: () => envs
+      }
+    ]
+
+    inquirer.prompt(questions.filter((q) => !opts[q.name]))
+    .then((inputs) => merge({}, inputs, opts))
+    .then(configRemove)
+  } else {
+    if (!opts.env) { console.log('no API found, cannot load available aliases') }
+
+    configRemove(opts)
+  }
+}
diff --git a/src/commands/config/set.js b/src/commands/config/set.js
new file mode 100644
index 0000000..1e05fb0
--- /dev/null
+++ b/src/commands/config/set.js
@@ -0,0 +1,42 @@
+import inquirer from 'inquirer'
+import configSet from '../../config-set'
+import * as load from '../../util/load'
+import merge from 'lodash.merge'
+
+export const command = 'set <env> <vars...>'
+export const desc = 'Set environment variables for alias on AWS'
+export function builder (yargs) {
+  return yargs
+  .pkgConf('shep', process.cwd())
+  .example('shep config set beta FOO=bar', 'Set environment variable FOO with value BAR for alias beta')
+}
+
+export async function handler (opts) {
+  let envVars = {}
+  opts.vars.forEach((varPair) => {
+    const split = varPair.split('=')
+    envVars[split[0]] = split[1]
+  })
+  opts.vars = envVars
+
+  const envs = await load.envs()
+
+  if (envs && envs.length > 0) {
+    const questions = [
+      {
+        name: 'env',
+        message: 'Environment',
+        type: 'list',
+        choices: () => envs
+      }
+    ]
+
+    inquirer.prompt(questions.filter((q) => !opts[q.name]))
+    .then((inputs) => merge({}, inputs, opts))
+    .then(configSet)
+  } else {
+    if (!opts.env) { console.log('no API found, cannot load available aliases') }
+
+    configSet(opts)
+  }
+}
diff --git a/src/commands/deploy.js b/src/commands/deploy.js
index 664001c..9c97951 100644
--- a/src/commands/deploy.js
+++ b/src/commands/deploy.js
@@ -20,17 +20,25 @@ export function builder (yargs) {
   .example('shep deploy beta *-user', 'Deploy only functions matching the pattern *-user')
 }
 
-export function handler (opts) {
-  const questions = [
-    {
-      name: 'env',
-      message: 'Environment',
-      type: 'list',
-      choices: () => load.envs()
-    }
-  ]
+export async function handler (opts) {
+  const envs = await load.envs()
 
-  inquirer.prompt(questions.filter((q) => !opts[q.name]))
-  .then((inputs) => merge({}, inputs, opts))
-  .then(deploy)
+  if (envs && envs.length > 0) {
+    const questions = [
+      {
+        name: 'env',
+        message: 'Environment',
+        type: 'list',
+        choices: () => envs
+      }
+    ]
+
+    inquirer.prompt(questions.filter((q) => !opts[q.name]))
+    .then((inputs) => merge({}, inputs, opts))
+    .then(deploy)
+  } else {
+    if (!opts.env) { console.log('no API found, cannot load available aliases') }
+
+    deploy(opts)
+  }
 }
diff --git a/src/commands/generate.js b/src/commands/generate.js
index b952849..7a5def1 100644
--- a/src/commands/generate.js
+++ b/src/commands/generate.js
@@ -6,12 +6,3 @@ export function builder (yargs) {
   .demand(1)
   .strict()
 }
-
-export function handler (args) {
-  // inquirer.prompt([{
-  //   type: 'list',
-  //   name: 'generator',
-  //   message: 'Select generator',
-  //   choices: ['endpoint', 'function']
-  // }])
-}
diff --git a/src/commands/logs.js b/src/commands/logs.js
index aa38e5a..5fab621 100644
--- a/src/commands/logs.js
+++ b/src/commands/logs.js
@@ -17,21 +17,37 @@ export function builder (yargs) {
   .example('shep logs production foo', 'Shows logs for the `foo` function in the production environment')
 }
 
-export function handler (opts) {
-  const questions = [
-    {
-      name: 'stage',
-      message: 'Stage',
-      type: 'list',
-      choices: () => load.envs()
-    },
-    {
-      name: 'name',
-      message: 'Function',
-      type: 'list',
-      choices: () => load.funcs()
-    }
-  ]
+export async function handler (opts) {
+  const envs = await load.envs()
+  let questions
+
+  if (envs && envs.length > 0) {
+    questions = [
+      {
+        name: 'stage',
+        message: 'Stage',
+        type: 'list',
+        choices: () => envs
+      },
+      {
+        name: 'name',
+        message: 'Function',
+        type: 'list',
+        choices: () => load.funcs()
+      }
+    ]
+  } else {
+    if (!opts.env) { console.log('no API found, cannot load available aliases') }
+
+    questions = [
+      {
+        name: 'name',
+        message: 'Function',
+        type: 'list',
+        choices: () => load.funcs()
+      }
+    ]
+  }
 
   inquirer.prompt(questions.filter((q) => !opts[q.name]))
   .then((inputs) => merge({}, inputs, opts))
diff --git a/src/commands/run.js b/src/commands/run.js
index b73c317..886d57f 100644
--- a/src/commands/run.js
+++ b/src/commands/run.js
@@ -19,7 +19,7 @@ export function builder (yargs) {
   .example('shep run foo --build', 'Builds the `foo` function and then runs it')
   .example('shep run foo --event default', 'Runs the `foo` function for just the `default` event')
   .example('shep run foo --environment production', 'Runs the `foo` function with production environment')
-  .example('DB_TABLE=custom shep run foo', 'Runs the `foo` function with process.env.DB_TABLE assigned to custom (vars declared this way will overwrite vals in your environments/env.json file)')
+  .example('DB_TABLE=custom shep run foo', 'Runs the `foo` function with process.env.DB_TABLE assigned to custom (vars declared this way will overwrite vals in your .env file)')
   .example('shep run \'*\'', 'Runs all functions for all events')
   .example('shep run \'foo-*\'', 'Runs all functions matching pattern `foo-*`')
 }
diff --git a/src/config-list/index.js b/src/config-list/index.js
new file mode 100644
index 0000000..24f4221
--- /dev/null
+++ b/src/config-list/index.js
@@ -0,0 +1,24 @@
+import getEnvironment from '../util/get-environment'
+import AWS from 'aws-sdk'
+
+export default function (opts) {
+  const name = opts.function
+  const env = opts.env || 'development'
+
+  AWS.config.update({region: opts.region})
+
+  console.log(`Listing environment variables for ${name}...`, '\n')
+
+  getEnvironment(env, name)
+  .then((vars) => {
+    if (vars && vars.length > 0) {
+      const envObj = vars[0]
+      Object.keys(envObj).forEach((key) => {
+        console.log(key, '=', envObj[key])
+      })
+      console.log('')
+    } else {
+      console.log(`No environment vars on AWS for ${name}`)
+    }
+  })
+}
diff --git a/src/config-remove/index.js b/src/config-remove/index.js
new file mode 100644
index 0000000..3da840d
--- /dev/null
+++ b/src/config-remove/index.js
@@ -0,0 +1,18 @@
+import remove from '../util/remove-environment'
+import listr from '../util/modules/listr'
+import AWS from 'aws-sdk'
+
+export default function (opts) {
+  const env = opts.env || 'development'
+
+  AWS.config.update({region: opts.region})
+
+  const tasks = listr([
+    {
+      title: 'Remove environment variables on functions in AWS',
+      task: () => remove(env, opts.vars)
+    }
+  ])
+
+  return tasks.run()
+}
diff --git a/src/config-set/index.js b/src/config-set/index.js
new file mode 100644
index 0000000..bcac48a
--- /dev/null
+++ b/src/config-set/index.js
@@ -0,0 +1,18 @@
+import upload from '../util/upload-environment'
+import listr from '../util/modules/listr'
+import AWS from 'aws-sdk'
+
+export default function (opts) {
+  const env = opts.env || 'development'
+
+  AWS.config.update({region: opts.region})
+
+  const tasks = listr([
+    {
+      title: 'Set environment variables on functions in AWS',
+      task: () => upload(env, opts.vars)
+    }
+  ])
+
+  return tasks.run()
+}
diff --git a/src/deploy/index.js b/src/deploy/index.js
index 0073ee3..d0298a4 100644
--- a/src/deploy/index.js
+++ b/src/deploy/index.js
@@ -33,7 +33,7 @@ export default function (opts) {
     },
     {
       title: 'Upload Functions to AWS',
-      task: () => upload(functions, env).tap((funcs) => { uploadedFuncs = funcs })
+      task: () => upload(functions).tap((funcs) => { uploadedFuncs = funcs })
     }
   ], opts.quiet)
 
diff --git a/src/new/index.js b/src/new/index.js
index 1f7966a..cfc3628 100644
--- a/src/new/index.js
+++ b/src/new/index.js
@@ -59,8 +59,7 @@ function setupIam (context) {
 
 function createSubDirs ({ path }) {
   return Promise.all([
-    mkdirp(path + '/functions'),
-    mkdirp(path + '/environments')
+    mkdirp(path + '/functions')
   ])
 }
 
@@ -69,10 +68,8 @@ function createFiles ({ path, arn, region }) {
 
   return Promise.all([
     writeFile(path + '/package.json', templates.pkg({ apiName: path, region, accountId })),
-    writeFile(path + '/environments/development.json', templates.env('development')),
-    writeFile(path + '/environments/beta.json', templates.env('beta')),
-    writeFile(path + '/environments/production.json', templates.env('production')),
     writeFile(path + '/.gitignore', templates.gitignore()),
+    writeFile(path + '/.env', templates.dotEnv()),
     writeFile(path + '/README.md', templates.readme(path)),
     writeFile(path + '/lambda.json', templates.lambda(arn)),
     writeFile(path + '/api.json', templates.api(path)),
diff --git a/src/new/templates.js b/src/new/templates.js
index dcafc2c..caffffd 100644
--- a/src/new/templates.js
+++ b/src/new/templates.js
@@ -10,10 +10,10 @@ export function api (apiName) {
 `
 }
 
-export function env (env) {
+export function dotEnv () {
   return `{
-  "ENV": "${env}",
-  "SECRET_KEY": "${env}-secret-key"
+  "ENV": "development",
+  "SECRET_KEY": "development-secret-key"
 }
 `
 }
@@ -21,7 +21,7 @@ export function env (env) {
 export function gitignore () {
   return `dist/*
 node_modules/*
-environments/*
+.env
 `
 }
 
diff --git a/src/run/index.js b/src/run/index.js
index 92a08c3..9c80518 100644
--- a/src/run/index.js
+++ b/src/run/index.js
@@ -5,6 +5,8 @@ import Promise from 'bluebird'
 import chalk from 'chalk'
 import AWS from 'aws-sdk'
 
+require('dotenv').config()
+
 import cliui from 'cliui'
 const ui = cliui({ width: 80 })
 
@@ -45,17 +47,6 @@ function runFunction (opts) {
     const lambdaConfig = load.lambdaConfig(name)
     const events = load.events(name, opts.event)
     const [ fileName, handler ] = lambdaConfig.Handler.split('.')
-    const defaultEnv = {
-      AWS_LAMBDA_FUNCTION_NAME: name,
-      AWS_LAMBDA_FUNCTION_VERSION: '$LATEST'
-    }
-    const envVars = load.envVars(env)
-
-    Object.assign(process.env, {
-      ...envVars,
-      ...defaultEnv,
-      ...process.env
-    })
 
     const context = {}
 
diff --git a/src/util/aws/api-gateway.js b/src/util/aws/api-gateway.js
index 6a1e80c..c6e2dc0 100644
--- a/src/util/aws/api-gateway.js
+++ b/src/util/aws/api-gateway.js
@@ -37,3 +37,13 @@ export function pushApi (api, id) {
     return apiGateway.importRestApi(params).promise().get('id')
   }
 }
+
+export function aliases (id) {
+  const apiGateway = new AWS.APIGateway()
+
+  let params = {
+    restApiId: id
+  }
+
+  return apiGateway.getStages(params).promise()
+}
diff --git a/src/util/aws/lambda.js b/src/util/aws/lambda.js
index 5582e70..a00eeb2 100644
--- a/src/util/aws/lambda.js
+++ b/src/util/aws/lambda.js
@@ -1,17 +1,16 @@
 import AWS from './'
 import merge from 'lodash.merge'
 
-export function putFunction (config, ZipFile, envVars) {
+export function putFunction (config, ZipFile) {
   const lambda = new AWS.Lambda()
 
   validateConfig(config)
 
   const FunctionName = config.FunctionName
   const Publish = true
-  const lambdaConfig = merge(config, { Environment: { Variables: envVars } })
 
   return lambda.getFunction({ FunctionName }).promise()
-  .then(() => lambda.updateFunctionConfiguration(lambdaConfig).promise())
+  .then(putEnvironment(config))
   .then(() => lambda.updateFunctionCode({ ZipFile, FunctionName, Publish }).promise())
   .catch({ code: 'ResourceNotFoundException' }, () => {
     const params = merge(config, { Publish, Code: { ZipFile } })
@@ -19,6 +18,77 @@ export function putFunction (config, ZipFile, envVars) {
   })
 }
 
+export function putEnvironment (env, config, envVars) {
+  const lambda = new AWS.Lambda()
+
+  validateConfig(config)
+
+  const params = {
+    FunctionName: config.FunctionName,
+    Qualifier: env
+  }
+
+  return lambda.getFunction(params).promise()
+  .then((awsFunction) => {
+    const envMap = mergeExistingEnv(awsFunction, envVars)
+    const lambdaConfig = merge(config, { Environment: { Variables: envMap } })
+    return lambda.updateFunctionConfiguration(lambdaConfig).promise()
+  })
+  .then(({ FunctionName }) => {
+    return lambda.publishVersion({ FunctionName }).promise()
+  })
+  .then((func) => {
+    setAlias(func, env)
+  })
+  .catch((e) => {
+    throw new Error(e)
+  })
+}
+
+export function removeEnvVars (env, config, envVars) {
+  const lambda = new AWS.Lambda()
+
+  validateConfig(config)
+
+  const params = {
+    FunctionName: config.FunctionName,
+    Qualifier: env
+  }
+
+  return lambda.getFunction(params).promise()
+  .then((awsFunction) => {
+    const envMap = deleteEnvVars(awsFunction, envVars)
+    const lambdaConfig = merge(config, { Environment: { Variables: envMap } })
+    return lambda.updateFunctionConfiguration(lambdaConfig).promise()
+  })
+  .then(({ FunctionName }) => {
+    return lambda.publishVersion({ FunctionName }).promise()
+  })
+  .then((func) => {
+    setAlias(func, env)
+  })
+  .catch((e) => {
+    throw new Error(e)
+  })
+}
+
+export function getEnvironment (env, { FunctionName }) {
+  const lambda = new AWS.Lambda()
+
+  const params = {
+    FunctionName,
+    Qualifier: env
+  }
+
+  return lambda.getFunction(params).promise()
+  .get('Configuration')
+  .get('Environment')
+  .get('Variables')
+  .catch((e) => {
+    throw new Error(`No environment variables exist for ${FunctionName}`)
+  })
+}
+
 export function getAliasVersion ({ functionName, aliasName }) {
   const lambda = new AWS.Lambda()
 
@@ -74,3 +144,22 @@ function validateConfig (config) {
     throw new Error('You need to specify a valid Role for your lambda functions. See the shep README for details.')
   }
 }
+
+function mergeExistingEnv (awsFunction, envVars = {}) {
+  if (awsFunction.Configuration.Environment) {
+    return merge(awsFunction.Configuration.Environment.Variables, envVars)
+  } else {
+    return envVars
+  }
+}
+
+function deleteEnvVars (awsFunction, envVars) {
+  envVars.forEach((envVar) => {
+    try {
+      delete awsFunction.Configuration.Environment.Variables[envVar]
+    } catch (e) {
+      throw new Error(`Variable ${envVar} does not exist on AWS`)
+    }
+  })
+  return awsFunction.Configuration.Environment.Variables
+}
diff --git a/src/util/get-environment.js b/src/util/get-environment.js
new file mode 100644
index 0000000..418cf3e
--- /dev/null
+++ b/src/util/get-environment.js
@@ -0,0 +1,9 @@
+import { getEnvironment } from './aws/lambda'
+import { lambdaConfig, funcs } from './load'
+
+export default async function (env, name) {
+  const fns = await funcs(name)
+  return Promise.all(fns.map(async (func) => {
+    return await getEnvironment(env, lambdaConfig(func))
+  }))
+}
diff --git a/src/util/load.js b/src/util/load.js
index 501bf77..2e5f541 100644
--- a/src/util/load.js
+++ b/src/util/load.js
@@ -1,9 +1,14 @@
 import { readdirSync, readJSONSync } from './modules/fs'
 import minimatch from 'minimatch'
+import { aliases } from './aws/api-gateway'
 
-export function envs () {
-  return readdirSync('environments')
-  .map((file) => file.split('.').shift())
+export async function envs () {
+  const api = this.api()
+  if (api && api.id) {
+    return aliases(api.id)
+  } else {
+    return []
+  }
 }
 
 export function events (func, eventName) {
@@ -19,7 +24,7 @@ export function events (func, eventName) {
   return events
 }
 
-export function funcs (pattern = '*') {
+export async function funcs (pattern = '*') {
   const funcs = readdirSync('functions').filter(minimatch.filter(pattern))
   if (funcs.length === 0) {
     throw new Error(`No functions found matching patterns: ${JSON.stringify(funcs)}`)
@@ -47,24 +52,6 @@ export function api () {
   }
 }
 
-export function envVars (env) {
-  let envConfig
-  try {
-    envConfig = readJSONSync(`environments/${env}.json`)
-  } catch (e) {
-    return {}
-  }
-
-  Object.keys(envConfig).forEach(key => {
-    let val = envConfig[key]
-    if (typeof val !== 'string') {
-      throw new Error(`Config value '${val}' from key '${key}' must be a string`)
-    }
-  })
-
-  return envConfig
-}
-
 export function babelrc () {
   return readJSONSync('.babelrc')
 }
diff --git a/src/util/remove-environment.js b/src/util/remove-environment.js
new file mode 100644
index 0000000..4adc4ea
--- /dev/null
+++ b/src/util/remove-environment.js
@@ -0,0 +1,12 @@
+import Promise from 'bluebird'
+import { removeEnvVars } from './aws/lambda'
+import { lambdaConfig, funcs } from './load'
+
+const pattern = '*'
+
+export default async function (env, vars) {
+  const fns = await funcs(pattern)
+  return Promise.all(fns.map(async (func) => {
+    return await removeEnvVars(env, lambdaConfig(func), vars)
+  }))
+}
diff --git a/src/util/upload-environment.js b/src/util/upload-environment.js
new file mode 100644
index 0000000..cf3adf3
--- /dev/null
+++ b/src/util/upload-environment.js
@@ -0,0 +1,12 @@
+import Promise from 'bluebird'
+import { putEnvironment } from './aws/lambda'
+import { lambdaConfig, funcs } from './load'
+
+const pattern = '*'
+
+export default async function (env, vars) {
+  const fns = await funcs(pattern)
+  return Promise.all(fns.map(async (func) => {
+    return await putEnvironment(env, lambdaConfig(func), vars)
+  }))
+}
diff --git a/src/util/upload-functions.js b/src/util/upload-functions.js
index f70a52c..4fe0954 100644
--- a/src/util/upload-functions.js
+++ b/src/util/upload-functions.js
@@ -1,15 +1,14 @@
 import Promise from 'bluebird'
 import zipDir from './zip-dir'
 import { putFunction } from './aws/lambda'
-import { lambdaConfig, funcs, envVars } from './load'
+import { lambdaConfig, funcs } from './load'
 
-export default function (pattern, env) {
+export default function (pattern) {
   return Promise.resolve(funcs(pattern))
   .map((func) => {
     return Promise.join(
       lambdaConfig(func),
       zipDir(`dist/${func}`),
-      envVars(env),
       putFunction
     )
   })


Test patch:
diff --git a/test/config-list/index.js b/test/config-list/index.js
new file mode 100644
index 0000000..2f2f142
--- /dev/null
+++ b/test/config-list/index.js
@@ -0,0 +1,28 @@
+import test from 'ava'
+import td from '../helpers/testdouble'
+import Promise from 'bluebird'
+
+const pkg = {
+  name: 'foo',
+  shep: {
+    region: 'us-east-1'
+  }
+}
+const getEnvironmentResponse = [{
+  KEY: 'VALUE'
+}]
+const environment = 'development'
+const functionName = 'bar'
+const load = td.replace('../../src/util/load')
+td.when(load.lambdaConfig()).thenReturn(pkg)
+
+const getEnvironment = td.replace('../../src/util/get-environment')
+td.when(getEnvironment(environment, functionName)).thenReturn(Promise.resolve(getEnvironmentResponse))
+
+test.before(async () => {
+  return await require('../../src/config-list/index')({ env: environment, function: functionName })
+})
+
+test('Gets environment', () => {
+  td.verify(getEnvironment(environment, functionName))
+})
diff --git a/test/config-remove/index.js b/test/config-remove/index.js
new file mode 100644
index 0000000..0e7b16b
--- /dev/null
+++ b/test/config-remove/index.js
@@ -0,0 +1,23 @@
+import test from 'ava'
+import td from '../helpers/testdouble'
+
+const pkg = {
+  name: 'foo',
+  shep: {
+    region: 'us-east-1'
+  }
+}
+const environment = 'development'
+const envVars = ['KEY', 'KEY2']
+const load = td.replace('../../src/util/load')
+td.when(load.lambdaConfig()).thenReturn(pkg)
+
+const removeEnvironment = td.replace('../../src/util/remove-environment')
+
+test.before(async () => {
+  return await require('../../src/config-remove/index')({ env: environment, vars: envVars })
+})
+
+test('Calls removeEnvironment', () => {
+  td.verify(removeEnvironment(environment, envVars))
+})
diff --git a/test/config-set/index.js b/test/config-set/index.js
new file mode 100644
index 0000000..1311ba8
--- /dev/null
+++ b/test/config-set/index.js
@@ -0,0 +1,26 @@
+import test from 'ava'
+import td from '../helpers/testdouble'
+
+const pkg = {
+  name: 'foo',
+  shep: {
+    region: 'us-east-1'
+  }
+}
+const environment = 'development'
+const envVars = {
+  KEY: 'VALUE',
+  KEY2: 'VALUE2'
+}
+const load = td.replace('../../src/util/load')
+td.when(load.lambdaConfig()).thenReturn(pkg)
+
+const uploadEnvironment = td.replace('../../src/util/upload-environment')
+
+test.before(async () => {
+  return await require('../../src/config-set/index')({ env: environment, vars: envVars })
+})
+
+test('Calls uploadEnvironment', () => {
+  td.verify(uploadEnvironment(environment, envVars))
+})
diff --git a/test/deploy/index-no-api.js b/test/deploy/index-no-api.js
index e1b6830..4dfb5c5 100644
--- a/test/deploy/index-no-api.js
+++ b/test/deploy/index-no-api.js
@@ -15,7 +15,7 @@ const load = td.replace('../../src/util/load')
 td.when(load.api()).thenReturn()
 
 const upload = td.replace('../../src/util/upload-functions')
-td.when(upload(functions, env)).thenResolve(uploadedFuncs)
+td.when(upload(functions)).thenResolve(uploadedFuncs)
 
 test.before(() => {
   const shep = require('../../src/index')
diff --git a/test/deploy/index-no-build.js b/test/deploy/index-no-build.js
index 7741f0f..b8983fc 100644
--- a/test/deploy/index-no-build.js
+++ b/test/deploy/index-no-build.js
@@ -19,7 +19,7 @@ const push = td.replace('../../src/util/push-api')
 td.when(push(api), { ignoreExtraArgs: true }).thenResolve(apiId)
 
 const upload = td.replace('../../src/util/upload-functions')
-td.when(upload(functions, env)).thenResolve(uploadedFuncs)
+td.when(upload(functions)).thenResolve(uploadedFuncs)
 
 test.before(() => {
   const shep = require('../../src/index')
diff --git a/test/deploy/index.js b/test/deploy/index.js
index dd03f46..092ee26 100644
--- a/test/deploy/index.js
+++ b/test/deploy/index.js
@@ -19,7 +19,7 @@ const push = td.replace('../../src/util/push-api')
 td.when(push(api), { ignoreExtraArgs: true }).thenResolve(apiId)
 
 const upload = td.replace('../../src/util/upload-functions')
-td.when(upload(functions, env)).thenResolve(uploadedFuncs)
+td.when(upload(functions)).thenResolve(uploadedFuncs)
 
 test.before(() => {
   const shep = require('../../src/index')
diff --git a/test/new/index.js b/test/new/index.js
index 312fe28..cf26847 100644
--- a/test/new/index.js
+++ b/test/new/index.js
@@ -18,7 +18,6 @@ test('No calls to AWS if no rolename', () => {
 
 test(createdDir, `${path}/functions`)
 test(createdDir, `${path}/functions`)
-test(createdDir, `${path}/environments`)
 
 test(wroteFile, `${path}/lambda.json`)
 test(wroteFile, `${path}/api.json`)
@@ -26,6 +25,7 @@ test(wroteFile, `${path}/package.json`)
 test(wroteFile, `${path}/.gitignore`)
 test(wroteFile, `${path}/README.md`)
 test(wroteFile, `${path}/webpack.config.js`)
+test(wroteFile, `${path}/.env`)
 
 test(didExec, 'npm install')
 
diff --git a/test/util/load.js b/test/util/load.js
index 38871ac..8a30360 100644
--- a/test/util/load.js
+++ b/test/util/load.js
@@ -3,16 +3,20 @@ import * as load from '../../src/util/load'
 
 test.before(() => process.chdir('./test/fixtures'))
 
-test('Loads environments', (t) => {
-  t.deepEqual(load.envs().sort(), [ 'beta', 'prod' ])
+test('Loads environments', async (t) => {
+  const envs = await load.envs()
+
+  t.deepEqual(envs, [ ])
 })
 
-test('Loads functions', (t) => {
-  t.deepEqual(load.funcs().sort(), [ 'bar', 'foo' ])
+test('Loads functions', async (t) => {
+  const funcs = await load.funcs()
+  t.deepEqual(funcs.sort(), [ 'bar', 'foo' ])
 })
 
-test('Loads functions by pattern', (t) => {
-  t.deepEqual(load.funcs('f*'), [ 'foo' ])
+test('Loads functions by pattern', async (t) => {
+  const funcs = await load.funcs('f*')
+  t.deepEqual(funcs, [ 'foo' ])
 })
 
 test('Loads function events', (t) => {