Lambda on Terraform Container

# はじめに
この記事は[terraform Advent Calendar 2020](https://qiita.com/advent-calendar/2020/terraform)の15日目です。

もともとTerraformのテストについてつらつら書こうとしましたが、re:Invent2020で[Lambdaのコンテナサポート](https://aws.amazon.com/jp/blogs/news/new-for-aws-lambda-container-image-support/)されました
今回re:Invent2020で発表された中で、比較的反響がでかかった発表じゃないでしょうか
この波に乗らねば、ということで、Terraformをコンテナ化してLambdaで実行できるようにしてみました
※決してLambdaをTerraformで構築する記事ではありません


# 構成
今回の構成は下記です
![lambda_terraform.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31883/5f7083f4-0f1a-354c-dfd6-944b4d0ae9e5.png)

開発ではCloud9を利用して、内部でコンテナを使って確認を行い、本番ではECRにイメージをアップしてLambdaから利用するという形になります
今回は時間がなかったのでAPI Gatewayは未使用です


# ユースケース
ユースケースとしては下記の２つほどあるかと思ってます

- 共通実行基盤
- 監視

共通実行基盤としては、よくTerraformの実行基盤で悩む人がいると思うのですが、こちらを今回作成するLambdaを共通基盤とするのも一つの案かと思います
今回はただコードを持ってきて叩いてますが、これをCodeCommitやGithubで管理してるコードを引っ張ってくるようにすれば、最新コードをとってデプロイできるかと思います

監視としては、よくコードとリソースが乖離している問題があると思いますが、今回のLambdaを利用すれば、乖離したら検知できるようにできます。今回の作成したAPIを叩けば追加・変更・削除のリソース数が取れるので、PrometheusでもDatadogでも利用すれば0じゃない時にアラートを出せるかと思います


# リポジトリ
対象のリポジトリは下記です
https://github.com/Yusuke-Shimizu/terraform_on_lambda
記事読むのだるいって人はcloneして使ってください
Dockerfileと.envrcの値は各自修正してご利用ください（アクセスキーとシークレットキー書いたままプッシュしちゃダメだよ！）

# コンテナイメージ
コンテナをLambdaに乗っける場合、下記のどちらかで構築する必要があります

- [ベースイメージ](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-images.html#runtimes-images-lp)を利用
- [Lambda ランタイム API](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-api.html)を実装

ランタイムAPIは必要なAPIを作る必要があるので、柔軟性はありますが手間が多いです
ベースイメージの方が、FROMに入れて追加していくだけで簡単なため、今回はこちらを利用します


# ベースイメージの選定
今回はPythonを利用してTerraformを実行していきます
Terraform自体がGoなので、Goでやろうとしたのですが、いかんせんGoの経験が少ないのと、Lambda on Goの情報も少ないので断念しました。。
[Amazon ECR Public Gallery](https://gallery.ecr.aws/?searchTerm=lambda&verified=verified)を参考に下記のようにベースイメージを決めました

```Dockerfile
FROM public.ecr.aws/lambda/python:3.8
...
```

ちなみに、このFROMの `public.ecr.aws` もre:Invent2020で発表された[ECR Public](https://aws.amazon.com/jp/blogs/aws/amazon-ecr-public-a-new-public-container-registry/)です
簡単にいうと、AWS版のDocker Hubです

# Terraformのインストール
上記のPythonイメージはPythonの実行しか出来ないため、Terraformを実行できるようにする必要があります
そのため、下記のようにzipをインストールしてterraformの実行をできるようにします

```Dockerfile
ARG terraform_version="0.14.2"
ADD https://releases.hashicorp.com/terraform/${terraform_version}/terraform_${terraform_version}_linux_amd64.zip terraform_${terraform_version}_linux_amd64.zip
RUN yum -y install unzip wget
RUN unzip ./terraform_${terraform_version}_linux_amd64.zip -d /usr/local/bin/
RUN rm -f ./terraform_${terraform_version}_linux_amd64.zip
ENV TF_DATA_DIR /tmp
```

## 実行ディレクトリ
Lambdaは実行するとき、/var/taskディレクトリで実行するのですが、基本的にこのディレクトリでファイルを作成したり修正することはできません
参考：https://stackoverflow.com/questions/45608923/aws-lambda-errormessage-errno-30-read-only-file-system-drive-python-quic

そのため、最後の `ENV TF_DATA_DIR /tmp` でTFのデータ出力先を変更してます
参考：https://www.terraform.io/docs/commands/environment-variables.html


## Dockerfileの残り
あとはAWSのキーや必要なファイルをコピーしてhandlerを実行するだけでDockerfileは完了です
AWSキーは本来はLambdaのロールを利用すれば良いのですが、ローカル検証も合わせて実行したいので、とりあえずで入れてます
本来はここもsts化するなり外部から環境変数を注入するタイプにしたほうが良いと思います

```Dockerfile
ENV AWS_ACCESS_KEY_ID 【アクセスキー】
ENV AWS_SECRET_ACCESS_KEY 【シークレットキー】

# copy files
COPY app.py ${LAMBDA_TASK_ROOT}
COPY main.tf ${LAMBDA_TASK_ROOT}

CMD [ "app.handler" ]
```

# Python
Pythonコードとしては、handle関数をapp.pyに作成すれば良いです
やってることとしては、tfファイルを/tmpにコピーして、terraformを初期化してplanを実行してます
/tmpに移動する理由としては、 `$ terraform init` を実行したタイミングで、カレントディレクトリにファイルを作成するのですが、上記で説明したように/tmp以外でファイル作成が出来ないので、移動してから実行としています

```python
def handler(event, context): 
    cmd('./', "cp ./main.tf /tmp/")
    cmd('/tmp/', "terraform init --upgrade")
    result = cmd('/tmp/', "terraform plan")
    last_result = extraction(result)
    return last_result
```

planの結果は下記でaddやchangeの数を正規表現でとってきて、dictで返すようにしてます

```python
def extraction(plan):
    print(plan)
    change_state = {'add': 0, 'change': 0, 'destroy': 0}
    if "No changes" in plan:
        return change_state
    elif "Plan" in plan:
        line_extraction = re.findall("Plan.*", plan)
        result = "".join(line_extraction)
        
        change_state['add'] = int(re.findall('(\d)\sto\sadd', result)[0])
        change_state['change'] = int(re.findall('(\d)\sto\schange', result)[0])
        change_state['destroy'] = int(re.findall('(\d)\sto\sdestroy', result)[0])
        return change_state
    elif "Error" in plan:
        line_extraction = re.findall("Error.*", plan)
        result = "".join(line_extraction)
        return result
    else:
        result = "予期せぬエラーです。ログを確認して下さい。"
        return result
```

# Terraform
Terraformのコードは簡単にS3バケットの作成にしました

```terraform
provider "aws" {
  region  = "ap-northeast-1"
}

resource "aws_s3_bucket" "default" {
  bucket = "created-by-lambda"
  acl    = "private"
}
```

# ローカル(Cloud9)での実行
下記のように実行して、コンテナを起動します

```shell-session
$ export REPOSITORY_NAME=【リポジトリ名】
$ docker build -t ${REPOSITORY_NAME} .
$ docker run --rm -p 9000:8080 ${REPOSITORY_NAME}
```

そして、ローカルの9000番ポートを叩くと、下記のようにS3が作成されるため、addに1が入った状態でかえってきました

```shell-session
$ curl -sd '{}' http://localhost:9000/2015-03-31/functions/function/invocations | jq .
{
  "add": 1,
  "change": 0,
  "destroy": 0
}
```

# ECRへアップ
上記で設定した `REPOSITORY_NAME` の名前でECRのリポジトリを作成します
その後、下記でイメージをプッシュすると、ECRへイメージがlatestで上がっています

```shell-session
$ export AWS_ACCOUNT_ID=$(aws sts get-caller-identity | jq -r .Account)
$ export REGISTRY_URL=${AWS_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com
$ aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin ${REGISTRY_URL}
$ docker tag ${REPOSITORY_NAME} ${REGISTRY_URL}/${REPOSITORY_NAME}
$ docker push ${REGISTRY_URL}/${REPOSITORY_NAME}
```

# Lambdaで実行
## Lambdaの作成
まず実行基盤のLambdaを作成します
といっても、コンソールからポチポチやっていけば良いです
自動化するならsamかCDKが良いかと思います（個人的にはCDK推し）

コンソールからだと、関数の作成からコンテナイメージを選択し、関数名適当に決めてコンテナイメージも「画像を参照」ボタンから対象のリポジトリのlatestを選択すればOKです

一点注意しないといけないのが、今回のTerraformの実行は時間がかかってメモリも多少食うので、下記のように設定してください
タイムアウト：1分
メモリ：4GB


## 実行
さて、準備が出来たので、ようやくLambdaからコンテナのTerraformを叩いてみましょう
こちらもコンソールから軽くテスト出来るので、こちらで試してみましょう

テストイベントは何でも良いので、デフォのものを利用して実行してみました
すると、下記のようにjsonでTerraformのplan結果が帰ってきました
![スクリーンショット 2020-12-15 18.50.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31883/53fb1ece-0ddf-5480-b9f3-e30ff512287f.png)
これを利用すれば、外部からこのAPIを叩いて、現状のリソースとの差分がないかを監視することが出来ます

# さいごに
Lambdaがコンテナで実行できるようになったので、Terraformを入れて実行してみました
今後は、API Gatewayと連携してAPI化して、Terraformの監視をしたり、planだけじゃなくapplyを実行するアプリも作っていきたいですね