AWS-CLIとSAM-CLI(with NodeJS)の環境を簡単に作るための俺のVagrantfileを公開してみる

# 概要

ケースバイケースかとは思いますが、個人的にはAWS-CLIの環境はプロジェクト別に分けて仮想環境に閉じ込めた方が便利かと思っています。
awsコマンドを打つときにいちいちprofileを指定しなくて済みますし、ホストOS側の環境を汚さなくて済むので。

今回はAWS-CLIの環境と一緒に、[SAM(ServelessApplicationModel)](https://github.com/awslabs/serverless-application-model) のCLIを使った開発環境を仮想OS上に簡単に構築するための私の"Vagrantfile"を、VSCodeを使ったデバッグ方法などと一緒に公開してみようと思います。
Vagrantfileというのは[Vagrant](https://www.vagrantup.com/) の設定ファイルです。
[VirtualBox](https://www.virtualbox.org/)と[Vagrant](https://www.vagrantup.com/) がinstallされている環境があれば"vagrant up"というコマンド一発で初期の開発環境構築作業が完了します。

当記事で作成される環境のLambdaのランタイムはnode10(10.15)です。

参考：[SAM(ServelessApplicationModel)](https://github.com/awslabs/serverless-application-model)
参考：[AWS Lambda ランタイム](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/lambda-runtimes.html)

※ Vagrantfileの中身は長いのでこの記事の最後の方に乗せてますが、[私のGithubの公開リポジトリ](https://github.com/kinocoffeeblack/my-vagrants/tree/master/aws-sam-cli-node10)でも公開しています。

## 特徴
"vagrant up"すると最初のプロビジョニングでLinux(CentOS7)上にAWS-CLIやSAM-CLI、DynamoDB-localなどが直ぐに使える環境が出来ます。
AWS-CLIに必要なpython3、SAM-CLIに必要なdockerなども最初にインストールされます。
Node(10.15.3)もインストールされます。
**下にVSCodeでのデバッグのやり方などいろいろ説明を書いてはおりますが、主に伝えたいことは「virtualboxとvagrantがあれば、このVagantfileを使ってvagrant upすればaws cliとsam cliの環境ができるYO！」ということ、だだそれだけです。**

[SAM-CLI](https://github.com/awslabs/aws-sam-cli)
[Dynamodb-local](https://hub.docker.com/r/amazon/dynamodb-local/)

# この環境を使った開発・デバッグについて(概要)
VirtualBoxの共有フォルダの機能を使って、ソースコードの編集はホストOS側のVSCode(VisualStudioCode)などのエディタで行えます。
AWS-CLIやSAMのコマンドを実行したりするのは"vagrant ssh"コマンドでゲストOSのLinuxにログインして行います。

"sam local invoke"で仮想環境上でLambdaを実行できますし、"yarn test"などでテストコードからLambdaのコードをテストすることもできます。
どちらの方法でもブレークポイントを張ってステップ実行ができます（詳細は後述）が、"sam local"を使った実行はゲストOS上、"yarn"あるいは"npm"を使ったUNITテストの実行はホストOSからになります。

VirtualBoxの共有フォルダの機能を使うのでGuest Additionsのプラグインが必要になります。
vagrantで使う場合、下記コマンドでインストールできます。

```
vagrant plugin install vagrant-vbguest
```
https://github.com/dotless-de/vagrant-vbguest

## 共有フォルダの場所
ゲストOS上の<b>"/vagrant"</b>がホストOS上のVagrantfileがあるディレクトリとの共有ディレクトリになっています(そういう設定にしています。Vagrantfileの"config.vm.synced_folder"の設定を変えれば変更できます)。

Vagrantfileがあるディレクトリの下にソースコードなどのファイルを置くと、ゲストOS上に"vagrant ssh"でログインした後は"/vagrant"というディレクトリの下で見つけることができるはずです。
SAMのコマンドを打つ場所は基本的には"/vagrant"の下にあるソースコードのディレクトリの下に移動して、ということになります。




# Vagrantfileの使い方
VagrantとVirtualBoxがインストールされていて、Guest Additionsプラグインもインストールされていれば、Vagrantfileがあるディレクトリで

```
vagrant up
```

というコマンドを実行するだけです（"centos/7"という公式のBoxを使っているのでboxの追加なども必要ないはずです）。
特に指定がない場合は最初の"vagrant up"実行時だけVagrantfileに書いてあるプロビジョニングが実行されるので、そこそこ時間がかかります。
最後に下記のようにインストールされたAWS-CLIやSAM-CLIなどのバージョンが出力されれば無事成功しています(最後に"aws configure"は自分でやってね。とechoするようにしてます)。

![versions.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41110/9492ee32-720b-400a-97c3-8964defb0f55.png)

後は、起動した仮想環境(CentOS7)に"vagrant ssh"でログインして"aws configure"コマンドを実行してご自身のAWSアカウントのIAMユーザを使ってAWS-CLIを使える環境に仕上げて下さい。

```
aws configure
```

サーバレス開発用のIAMユーザに多要素認証（MFA）をかけて制限を厳しくしている環境の場合、上記の"aws configure"でIAMユーザのACCESSKEYとSECRET_ACCESS_KEYを設定するだけでは足りない場合があります。
この場合は"もうひと手間"が必要になるのですが、詳細は後述の「【補足】IAMユーザにMFA(多要素認証)をかけている場合」で説明しています。

ちなみに、起動した仮想環境を停止したい場合は

```
vagrant halt
```

を実行します。

作成した仮想環境を削除してしまいたい場合は

```
vagrant destroy
```

を実行します。


# Lambda(NodeJSランタイム)のデバッグについて（sam local invoke）
こんな感じでローカルの仮想環境上でLambdaを実行できます。

```
sam local invoke HelloWorldFunction -e HelloWorldFunction/event.json
```

"-e"で指定しているのはイベントファイルで、"sam local generate-event"コマンドでテンプレートを出力できます。
上記は一度出力したものをevent.jsonファイルにリダイレクトして変更を加えたものをパラメータで指定するような例ですが、下記のようにパイプで渡すこともできます。
（Lambdaのイベントトリガーとなり得るサービスはいろいろありますが、RestAPIのバックエンドとする場合は下記のようにapigatewayのイベントのjsonを指定することになります）

```
sam local generate-event apigateway aws-proxy | sam local invoke HelloWorldFunction
```

また、下記のように"-d"オプションでポートを指定することでデバッグ用に起動することができます。
下記の場合5858ポートで待ち受け状態になるので、VSCodeなどでブレークポイントを張って5858ポートにアタッチすることでデバッガでステップ実行できます。

```
sam local invoke HelloWorldFunction -e HelloWorldFunction/event.json -d 5858
```

VSCodeのデバッガから待ち受け状態になっている5858ポートにアタッチする場合の起動設定は下記のようになります。
(.vscode/launch.jsonの設定例)

```json
{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Attach to HelloWorldFunction(5858)",
      "type": "node",
      "request": "attach",
      "address": "localhost",
      "port": 5858,
      "localRoot": "${workspaceRoot}/HelloWorldFunction",
      "remoteRoot": "/var/task",
      "protocol": "inspector",
      "stopOnEntry": false
    }
  ]
}
```
<b>（補足）</b>

上記設定の"localRoot"に設定するパスは".vscode/launch.json"がどこにあるか（VSCodeのワークスペースをどこにしているか）で異なります。上記は、SAMのテンプレート(template.yaml)と同じ場所に".vscode"がある場合の指定例です。

ゲストOS上の5858ポートで待ち受け状態にしたものに、ホストOSのデバッガ(VSCode)からアタッチするには5858番ポートのポート転送が必要です。
最後の方で補足していますが、Vagrantfileで5858ポートの転送設定を行っています。

ホストOS上にもyarn（あるいはnpm）の環境を作って"yarn install"すれば"sam local invoke"ではなくホストOS上で"yarn test --inspect-brk"などでmochaでテストコードからデバックすることもできます。その手順はここでは割愛しますがVSCodeのデバッガの起動設定には下記のような設定を追加することになります。

(.vscode/launch.jsonの追加分の設定例 ※Mocha用)

```json
    {
      "name": "Attach to Node(9229)",
      "type": "node",
      "request": "attach",
      "address": "localhost",
      "port": 9229,
      "stopOnEntry": false
    }
```

# Lambdaのビルドについて

zipパッケージングの前に"sam build"コマンドでビルドします。内部的には"npm install --production"が実行されているようです。
余談ですが"sam build"自体がわりと新しいコマンドです（2018年11月くらいにSAM-CLIに追加されたはず）。

```
sam build \
 --build-dir ~/sam-build/hello-world
```

<font color="red">（注意点）</font>
上記の例では"--build-dir"でビルド先ディレクトリをホームディレクトリ下の"sam-build/hello-world"に明示的に指定しています。
指定しない場合は".aws-sam"というディレクトリがSAMのディレクトリの直下に作成されてビルド成果物が出力されます。

Windowsの環境でVirtualBoxの共有ディレクトリ上で"npm install"などを行うとゲストOS上でLinuxのシンボリックリンクを作ろうとしてエラーになることがあります（hello-worldではエラーにならないかもですが）。
ここで"--build-dir"を指定しているのは、ゲストOS上での"npm install"時の共有フォルダとシンボリックリンクに起因するエラーを回避するためにvagrantの共有ディレクトリ("/vagrant")の外のディレクトリをビルドディレクトリとするためです。

参考（sam buildのオプション）：
https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-cli-command-reference-sam-build.html

# デプロイについて（sam package, sam deploy）

こんな感じでパッケージングしてます(LAMBDA_UPLOAD_BUCKET_NAMEの部分は各自で変えて下さい)。
Lambdaがzip化されてS3バケットにアップロードされます。
--template-fileは、build時に指定したディレクトリと合うようにします。

```
sam package \
    --output-template-file packaged.yaml \
    --s3-bucket LAMBDA_UPLOAD_BUCKET_NAME \
    --template-file ~/sam-build/hello-world/template.yaml
```

こんな感じでデプロイします。(STACK_NAMEの部分は各自で変えて下さい)
STACK_NAMEはCloudFormationのスタック名で、template.yamlの記述に従ってLambdaや各種リソースが作成・更新されます。

```
sam deploy \
    --template-file packaged.yaml \
    --stack-name STACK_NAME \
    --capabilities CAPABILITY_IAM
```

---

sam buildからpackage、deployの流れはまずは初期テンプレート(hello-world)で実際にやって見るといいと思います。
例えば"sam_sample_node10"というスタック名でnodejs10.xのLambdaの初期テンプレートを生成するには下記コマンドを実行すればよいです。

```
sam init --runtime nodejs10.x -n sam_sample_node10
```

仮想OS上の"/vagrant"の下などで上記コマンドを実行すると、出力されたソースコードなどをホストOS上で閲覧・編集できるはずです。sam initを実行すると初期状態で"hello-world"というサンプルのLambda関数が"template.yaml"などと一緒に出来ているはずです。

お試しで作ってみたリソース一式を削除したい場合はcloudformationのコマンド"aws cloudformation delete-stack"を実行します。
SAMのテンプレート(template.yaml)で定義されているAWSのリソース一式がcloudformationの機能で削除されます。

```
aws cloudformation delete-stack --stack-name sam_sample_node10
```


# Vagrantfileの中身

Githubにも公開していますので、生ファイルはそちらからダウンロードしてください。

```
Vagrant.configure("2") do |config|
  config.vm.box = "centos/7"

  # debug port with "sam local invoke"
  config.vm.network "forwarded_port", guest: 5858, host: 5858

  # forward dynamoDB-local port. (http://localhost:8000/shell/).
  # comment next line, if you don't want to use dynamoDB-local.
  config.vm.network "forwarded_port", guest: 8000, host: 8000

  config.vm.network "private_network", ip: "192.168.33.14"
  config.vm.synced_folder ".", "/vagrant", type: "virtualbox"
  config.vm.provider "virtualbox" do |vb|
    vb.gui = false
    vb.memory = 512
    vb.cpus = 2
  end

  config.vm.provision "shell", inline: <<-SHELL
    ############################
    # provision as root
    ############################
    ## change locale to ja_JP
    localedef -f UTF-8 -i ja_JP ja_JP
    localectl set-locale LANG=ja_JP.utf8

    timedatectl set-timezone Asia/Tokyo
    yum update -y
    yum install -y git gcc gcc-c++ openssl-devel readline-devel wget zip unzip bzip2-devel sqlite sqlite-devel zlib-devel libffi-devel

    ## install python3.6 as python3 (by epel)
    yum install -y epel-release
    yum install -y python36 python36-devel.x86_64
    ln -s /bin/python36 /bin/python3
    curl "https://bootstrap.pypa.io/get-pip.py" -o "get-pip.py"
    python3 get-pip.py

    ## install docker for AWS SAM
    yum install -y yum-utils device-mapper-persistent-data lvm2
    yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
    yum install -y docker-ce
    systemctl start docker
    systemctl enable docker
  SHELL

  config.vm.provision "shell", privileged: false, inline: <<-SHELL
    ############################
    # provision as vagrant
    ############################
    ## install anyenv
    git clone https://github.com/anyenv/anyenv.git ~/.anyenv
    echo 'export PATH="$HOME/.anyenv/bin:$PATH"' >> $HOME/.bash_profile
    echo 'eval "$(anyenv init -)"' >> $HOME/.bash_profile
    source $HOME/.bash_profile
    anyenv install --force-init

    ## install xxenv(node)
    anyenv install nodenv
    source $HOME/.bash_profile

    ## nodejs
    nodenv install 10.15.3
    nodenv global 10.15.3
    nodenv rehash
    npm install -g npm
    ## install yarn
    npm install -g yarn
    nodenv rehash

    ## aws-cli
    pip install awscli --user
    pip install aws-sam-cli --user
    ## aws_completer
    echo "complete -C '/home/vagrant/.local/bin/aws_completer' aws" >> $HOME/.bashrc

    ## add vagrant user to docker group
    sudo gpasswd -a $USER docker

    ## docker run
    ## comment next line, if you don't want to use dynamoDB-local.
    sudo docker run --restart=always --name dynamodb-local -p 8000:8000 -d amazon/dynamodb-local -jar DynamoDBLocal.jar -inMemory -sharedDb

    ## create shell-script symbolic-link for AWS-CLI with MFA.
    ln -s /vagrant/getSessionToken.sh $HOME/getSessionToken.sh

    echo "##### VERSIONS #####"
    echo "python3: `python3 --version`"
    echo "node: `node --version`"
    echo "npm: `npm --version`"
    echo "yarn: `yarn --version`"
    echo "docker: `docker --version`"
    echo "AWS-CLI: `aws --version`"
    echo "SAM-CLI: `sam --version`"

    cat << EOS

###########################################################
## TODO! "aws configure" and set user settings by yourself.
###########################################################
EOS

  SHELL
end
```

# Vagrantfileの補足説明
コメント行と一緒に中身をさらっと見れば大体何をやっているか判ると思いますが、軽く補足しておきます。

## private_network指定
あまり使われていないであろうと勝手に判断したIPアドレス(192.168.33.14)を使うように設定しています。既に他のVMをローカルで動かしているなどで被っている場合は末尾の"14"の部分などを変えて下さい。

```
config.vm.network "private_network", ip: "192.168.33.14"
```

## 共有フォルダ設定
"config.vm.synced_folder"の設定で、ホストOSのVagrantfileがあるディレクトリと仮想環境のゲストOS上の"/vagrant"が共有されるように設定されています。
なので、Vagrantfileがあるディレクトリの下のソースコードなどのファイルは、ゲストOS上では/vagrantの下にそれがあります。

```
  config.vm.synced_folder ".", "/vagrant", type: "virtualbox"
```

## ポート転送設定(5858)
下記の設定で5858ポートの転送設定をしています。

```
  # debug port with "sam local invoke"
  config.vm.network "forwarded_port", guest: 5858, host: 5858
```

ホストOS上で5858ポートにアクセスすると、ゲストOS上の5858ポートに転送されます。
ゲストOS上で"sam local invoke"を"-d 5858"オプションで呼び出すと5858ポートで待ち受け状態になるので、ホストOSのデバッガ(VSCodeなど)で5858ポートにアタッチすることでブレークポイントを張ってのデバッグが可能になります。

## ポート転送設定(8000)
サーバレスアーキテクチャではだいたいDBにDynamoDBを使うと思うので、Dockerコンテナ上で動くdynamodb-localもインストールしています。
dynamodb-localは8000番ポートで起動するように指定しているので、それにホストOS上からアクセスするための転送設定です。
8000番以外に変更したい場合は、ポート転送設定のポートを最後の方の"docker run"コマンドで指定している"-p 8000:8000"のパラメータと一緒に変更して下さい。

8000番で起動した場合は、dynamodb-localのWEBコンソールに
http://localhost:8000/shell/
でホストOS上のブラウザからもアクセスできるようになっているはずです。

↓ こんなヤツね
![dynamoLocal.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41110/091bada9-adb6-6fa5-b5b2-b0f4b1bde56a.png)


# 【補足】IAMユーザにMFA(多要素認証)をかけている場合
SAMやServerlessを使って開発する場合、開発に使うIAMユーザに管理者相当の大きな権限を付けることが多いと思います。
セキュリティのベストプラクティスに従ってIAMユーザのMFA（多要素認証）設定でセキュリティを厳しくしている場合、"aws configure"でAccessKeyとSecretAccessKeyなどを設定するだけではAWS CLIやSAM CLIなどのコマンドを使えず、"さらにもうひと手間"が必要になります。

具体的にはMFAデバイスの認証トークン（GoogleAuthenticatorなどで生成されるもの）を入力することで、セッションTokenなどの有効期限付きの一時的なクレデンシャルを払い出し、それらを使ってAWSのコマンドを実行します。

Githubには、上記のVagrantfileと一緒に"ひと手間"用に環境変数に一時的なクレデンシャルを保存するシェルスクリプトのテンプレート(getSessionToken.sh)が含まれています。
<font color="red">**シェルスクリプトの一部をご自分の環境に合わせた値に更新の上（※）、**</font>"vagrant ssh"でシェルにログインする度に一発だけスクリプトを実行することで環境変数に必要な情報が設定されます（少し面倒ですがシェルにログインする度に毎回必要な作業です）。

【シェルスクリプトの中身】

```shell
#!/bin/bash

# 呼び出し方例) source /vagrant/getSessionToken.sh XXXXXX(TOKEN_CODE)

TOKEN_CODE=${1}

if [ -z "$TOKEN_CODE" ]; then
   echo "please input TOKEN_CODE at first parameter."
   return
fi

# Edit to your Environment
AWS_ACCOUND_ID=YOUR_AWS_ACCOUNT_ID
AWS_IAM_USER=YOUR_USERNAME_WITH_MFA

# AWS_ACCESS_KEY_ID、AWS_SECRET_ACCESS_KEY、AWS_SESSION_TOKENを環境変数に設定
# "Expiration"は環境変数に必要ないので一時的にShell変数に設定して標準出力だけしている
# --duration-secondsは有効期限の指定(デフォルトが12h=43200)
eval `aws sts get-session-token --serial-number arn:aws:iam::${AWS_ACCOUND_ID}:mfa/${AWS_IAM_USER} --token-code ${TOKEN_CODE} --duration-seconds 43200 | awk '
 $1 == "\"AccessKeyId\":" { gsub(/\"/,""); gsub(/,/,""); print "export AWS_ACCESS_KEY_ID="$2 }
 $1 == "\"SecretAccessKey\":" { gsub(/\"/,""); gsub(/,/,""); print "export AWS_SECRET_ACCESS_KEY="$2 }
 $1 == "\"SessionToken\":" { gsub(/\"/,""); gsub(/,/,""); print "export AWS_SESSION_TOKEN="$2 }
 $1 == "\"Expiration\":" { gsub(/\"/,""); gsub(/,/,""); print "SESSION_TOKEN_EXPIRATION="$2 }
'`

if [ -z "$AWS_ACCESS_KEY_ID" ]; then
   echo "get-session-token failure."
   return
fi

echo "SUCCESS. SesstionToken-Expiration is ${SESSION_TOKEN_EXPIRATION}（JST: `date --date=${SESSION_TOKEN_EXPIRATION} '+%Y/%m/%d %H:%M:%S'`）"
```


※ 各自の環境に合わせて更新する部分(忘れずに！)

```shell
# Edit to your Environment
AWS_ACCOUND_ID=YOUR_AWS_ACCOUNT_ID
AWS_IAM_USER=YOUR_USERNAME_WITH_MFA
```

コマンド実行例) 

```
source /vagrant/getSessionToken.sh xxxxxx
```

※ 下の例のように"SUCCESS"と表示されればコマンドの実行に成功していますので試しに適当なawsコマンド("aws s3 ls"とか)を試してみて下さい
![console2.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41110/37cadaae-89da-9d6f-27b2-8999efce2b64.png)


スクリプト内で実行しているワンライナーは[こちらの記事](https://qiita.com/kter/items/9663457d4d27a3941655)を参考にさせて頂きました（ありがとうございます！）。


参考：
[チュートリアル: ユーザーが自分の認証情報および MFA を設定できるようにする](https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/tutorial_users-self-manage-mfa-and-creds.html)
[AWS CLI 経由で AWS リソースへのアクセスを認証するには、どのように MFA トークンを使用したらよいですか?](https://aws.amazon.com/jp/premiumsupport/knowledge-center/authenticate-mfa-cli/)

# 【補足2】vagrantのバージョンなど
vagrantは執筆時点（2019-10-20）で最新のバージョンで動作確認を行いました。

```
$ vagrant -v
Vagrant 2.2.6

$ vagrant plugin list
vagrant-vbguest (0.20.0, global)
  - Version Constraint: > 0
```

私の開発環境では特に問題は起こらないので保証はできませんが、もしすでに古いバージョンのvagrantをお使いで下記のようなエラーでフォルダ共有に失敗するようでしたら最新のvagrantをインストールして"vagrant plugin update"コマンドでプラグインのバージョンも更新してやればうまくいくかもしれません。

```
/sbin/mount.vboxsf: mounting failed with the error: No such device
```

# 【補足3】anyenv

Vagrantfileの中身を多少細かく見た方は気づいたかもしれませんが、プロビジョニング処理内でのnodejsのインストールには[anyenv](https://github.com/anyenv/anyenv)というものを使ってます。
anyenvについて詳しくは書きませんが、例えば"nodenv"コマンドでnodejsのバージョンを変えたときに"nodenv rehash"というコマンドを実行しないとバージョンの更新が反映されません。

バージョン変更などは稀にしか実施しない作業のせいか、たまにやるとrehashするの忘れて"おや？変わってないぞ？？"となることがあるので一応注意点として書いておきます（バージョン変えたら"rehash"）。