環境変数を持つLambdaをCloudFormationで管理する

# はじめに

AWSリソースをCloudFormationで管理すると便利というのは分かってはいるけど、LambdaのソースをCloudFormationで管理するのは色々ハードルがあってなかなか手が出せないという自分のような層向けに、まずは最小限のところからやってみようという意識低めの記事です。
まずはこのあたりから始めつつ、より良いベストプラクティスなどは勉強しながら成長させていきたい。

## 今回やりたいこと

- LambdaのソースコードをCloudFormationテンプレートから分離する
- Lambda環境変数をソースリポジトリから追い出す

## 今回触れないこと

- CodeDeployなどのCIとの統合
- SAMのお話

## 検証したバージョン

```
$ aws --version
aws-cli/1.16.28 Python/2.7.15 Darwin/18.6.0 botocore/1.12.18
```

# Lambdaリソースを作成するCloudFormationテンプレート

## ディレクトリ構造

今回は以下のような構造のNodeJS関数について考えます。

```
/project-root
 |- hello-http  #<= lambdaソースディレクトリ
     |- node_modules
     |- index.js
     |- package.json
     |- package-lock.json
 |- template.yml #<= CloudFormationテンプレートファイル
 |- settings.conf #<= パラメータ変数定義
 |- package.sh #<= packageコマンドスクリプト
 |- deploy.sh #<= deployコマンドスクリプト
```

### パラメータ

CloudFormationでは以下のようにパラメータ定義することで`aws cloudformation deploy`コマンド実行時にパラメータを渡すことができます。

```
AWSTemplateFormatVersion: 2010-09-09
Description: Lambda CloudFormation Template
Parameters:
  BaseStackName:
    Type: String
    Description: Base Stack Name
  ApiUrl:
    Type: String
    Description: API Url
    Default: https://api.example.net/api
  ApiToken:
    Type: String
    Description: API Token
    NoEcho: true
```

CloudFormationスタック内であまり表示したくないパラメータについては、`NoEcho: true`を設定しておくとパスワード扱いとなり、CloudFormationスタック上では非表示になります。
Lambda関数に環境変数として渡した場合はLambda関数上では見れてしまうので、今回のケースに関しては気休め程度という気はします。
Lambda関数内でよりセキュアにトークンなどを扱うにはやはりSecretsManagerあたりを使うのがよいのでしょうかね。
このあたりはまだ勉強中です。

### ベースとなるLambdaリソース

```
Resources:
  HttpCallFunction: 
    Type: 'AWS::Lambda::Function' 
    Properties: 
      Code: hello-http 
      Description: Lambda Function
      FunctionName: !Sub hello-http-${BaseStackName}
      Role: 
        Fn::GetAtt: [ LambdaExecutionRole, Arn ] 
      Runtime: nodejs8.10 
      Timeout: 25 
      Handler: index.handler 
```

ポイントとなるのは [Properties]の[Code]。
ローカルの相対パスディレクトリを指定しておきます。
この状態で `aws cloudformation package`コマンドを実行すると、

- ローカルファイルを固めてS3にアップロード
- CloudFormationのテンプレートファイル内のLambdaのコードをS3パスに変更

してくれます。

他には、`!Sub hello-http-${BaseStackName}`の部分はCloudFormationパラメータで指定して、デプロイ環境ごとにLambda関数を別々にデプロイすることを想定しています。


```
      Role: 
        Fn::GetAtt: [ LambdaExecutionRole, Arn ]
```

の部分は、別途作成するLambdaの実行ロールのARNを参照します。

#### packageコマンド

```
aws cloudformation package --s3-bucket $BUCKET --s3-prefix $PREFIX --template-file template.yml --output-template-file template-packaged.yml
```

| パラメータ | 内容 |
|:--|:--|
| --s3-bucket | ソースをアップロードするバケット名 |
| --s3-prefix | ソースをアップロードするプレフィックス(任意) |
| --template-file | 入力元となるテンプレートファイル |
| --output-template-file | 変換後のテンプレート出力先ファイル |

#### 変換後のパラメータ

```
  Properties:
    Code:
      S3Bucket: $BUCKET
      S3Key: $PREFIX/0123456ea0dab860708bb7c79c676278
```

### 実行ロールの設定

Lambdaファンクションの実行ロールも作成しておきます。

```
  LambdaExecutionRole: 
    Type: 'AWS::IAM::Role' 
    Properties: 
      AssumeRolePolicyDocument: 
        Version: 2012-10-17 
        Statement: 
          - 
            Effect: 'Allow' 
            Principal: 
              Service: 
                - 'lambda.amazonaws.com' 
            Action: 
              - 'sts:AssumeRole' 
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
```

最低限のロールとしてlambdaへの信頼関係の付与と、CloudWatchLogsへログを書き込むマネージドポリシーを設定しておきます。
他の権限が必要な場合はここに追加していきます。

## 環境変数のパラメータ化

CloudFormationパラメータをそのまま環境変数に渡すことができます。

```
    Properties: 
      ...
      Environment: 
        Variables:
          ApiUrl: !Ref ApiUrl
          ApiToken: !Ref ApiToken
```

これで、deploy時に渡したパラメータがLambdaの環境変数として設定されます。

#### deployコマンド

```
aws cloudformation deploy --template-file template-packaged.yml --stack-name hello-http-develop --capabilities CAPABILITY_NAMED_IAM CAPABILITY_IAM --parameter-overrides BaseStackName=develop ApiUrl=https://example.com/ ApiToken=xxxxx
```

| パラメータ | 内容 |
|:--|:--|
| --template-file | リソースを作成するテンプレートファイル |
| --stack-name | CloudFormationスタック名 |
| --parameter-overrides | パラメータに渡す値。Key=Value形式でスペース区切りで複数指定可能。 |

### package/deployシェルの作成

packageコマンド、deployコマンドはシェルスクリプトにしてしておくと便利です。
deployは引数に渡す環境変数を開発/本番環境ごとに分離しておくと事故も減らせて安心です。

#### packageスクリプト

```
#!/bin/sh
aws cloudformation package --s3-bucket $BUCKET --s3-prefix $PREFIX --template-file template.yml --output-template-file template-packaged.yml $OPTIONS
```

#### deployスクリプト

```
#!/bin/sh

CONF=$1
CONF=${CONF:=settings.conf}

source $CONF

aws cloudformation deploy --template-file template-packaged.yml --stack-name hello-http-$BaseStackName --capabilities CAPABILITY_NAMED_IAM CAPABILITY_IAM --parameter-overrides $PARAMS $OPTIONS
```

### confファイル (settings.conf)

```
BaseStackName=develop
OPTIONS=""
PARAMS="BaseStackName=develop ApiVersion=1.0 ApiUrl=http://localhost ApiToken=xxx"
```

本番環境用には`settings-production.conf`などを用意し、

```
$ ./deploy.sh settings-production.conf
```

を実行すると、本番環境用トークンなどが環境変数に設定されたLambdaが新しくデプロイするようにしておきます。

### 今回検証したテンプレートファイル全体

```
AWSTemplateFormatVersion: 2010-09-09
Description: Lambda CloudFormation Template
Parameters:
  BaseStackName:
    Type: String
    Description: Base Stack Name
  ApiVersion:
    Type: String
    Description: API Version
    Default: v1
  ApiUrl:
    Type: String
    Description: API Url
    Default: https://api.example.net/api
  ApiToken:
    Type: String
    Description: API Token
    NoEcho: true
Resources:
  HttpCallFunction: 
    Type: 'AWS::Lambda::Function' 
    Properties: 
      Code: hello-http 
      Description: Lambda Function
      FunctionName: !Sub hello-http-${BaseStackName} 
      Role: 
        Fn::GetAtt: [ LambdaExecutionRole, Arn ] 
      Runtime: nodejs8.10 
      Timeout: 25 
      Handler: index.handler 
      Environment: 
        Variables: 
          ApiVersion: !Ref ApiVersion
          ApiUrl: !Ref ApiUrl
          ApiToken: !Ref ApiToken
  LambdaExecutionRole: 
    Type: 'AWS::IAM::Role' 
    Properties: 
      AssumeRolePolicyDocument: 
        Version: 2012-10-17 
        Statement: 
          - 
            Effect: 'Allow' 
            Principal: 
              Service: 
                - 'lambda.amazonaws.com' 
            Action: 
              - 'sts:AssumeRole' 
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
```

今回作ったコードサンプル全体は[こちら](https://github.com/kazuhiro1982/hello-lambda-cloudformation)にも置いてあります。

# まとめ

ここまでできると、今まで手作業で作成していたLambdaの管理がCloudFormationで出来そうな気になってきました。
SAMとか関係ない細々したLambdaをどう管理するのかがよく分かっていなかったので、多少整理できた気がします。
`sam package`で出来ることはおおよそ`aws cloudformation package`でも出来るようですね。