はじめに
この記事はterraform Advent Calendar 2020の15日目です。
もともとTerraformのテストについてつらつら書こうとしましたが、re:Invent2020でLambdaのコンテナサポートされました
今回re:Invent2020で発表された中で、比較的反響がでかかった発表じゃないでしょうか
この波に乗らねば、ということで、Terraformをコンテナ化してLambdaで実行できるようにしてみました
※決してLambdaをTerraformで構築する記事ではありません
構成
開発ではCloud9を利用して、内部でコンテナを使って確認を行い、本番ではECRにイメージをアップしてLambdaから利用するという形になります
今回は時間がなかったのでAPI Gatewayは未使用です
ユースケース
ユースケースとしては下記の2つほどあるかと思ってます
- 共通実行基盤
- 監視
共通実行基盤としては、よくTerraformの実行基盤で悩む人がいると思うのですが、こちらを今回作成するLambdaを共通基盤とするのも一つの案かと思います
今回はただコードを持ってきて叩いてますが、これをCodeCommitやGithubで管理してるコードを引っ張ってくるようにすれば、最新コードをとってデプロイできるかと思います
監視としては、よくコードとリソースが乖離している問題があると思いますが、今回のLambdaを利用すれば、乖離したら検知できるようにできます。今回の作成したAPIを叩けば追加・変更・削除のリソース数が取れるので、PrometheusでもDatadogでも利用すれば0じゃない時にアラートを出せるかと思います
リポジトリ
対象のリポジトリは下記です
https://github.com/Yusuke-Shimizu/terraform_on_lambda
記事読むのだるいって人はcloneして使ってください
Dockerfileと.envrcの値は各自修正してご利用ください(アクセスキーとシークレットキー書いたままプッシュしちゃダメだよ!)
コンテナイメージ
コンテナをLambdaに乗っける場合、下記のどちらかで構築する必要があります
- ベースイメージを利用
- Lambda ランタイム APIを実装
ランタイムAPIは必要なAPIを作る必要があるので、柔軟性はありますが手間が多いです
ベースイメージの方が、FROMに入れて追加していくだけで簡単なため、今回はこちらを利用します
ベースイメージの選定
今回はPythonを利用してTerraformを実行していきます
Terraform自体がGoなので、Goでやろうとしたのですが、いかんせんGoの経験が少ないのと、Lambda on Goの情報も少ないので断念しました。。
Amazon ECR Public Galleryを参考に下記のようにベースイメージを決めました
FROM public.ecr.aws/lambda/python:3.8
...
ちなみに、このFROMの public.ecr.aws
もre:Invent2020で発表されたECR Publicです
簡単にいうと、AWS版のDocker Hubです
Terraformのインストール
上記のPythonイメージはPythonの実行しか出来ないため、Terraformを実行できるようにする必要があります
そのため、下記のようにzipをインストールしてterraformの実行をできるようにします
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化するなり外部から環境変数を注入するタイプにしたほうが良いと思います
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以外でファイル作成が出来ないので、移動してから実行としています
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で返すようにしてます
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バケットの作成にしました
provider "aws" {
region = "ap-northeast-1"
}
resource "aws_s3_bucket" "default" {
bucket = "created-by-lambda"
acl = "private"
}
ローカル(Cloud9)での実行
下記のように実行して、コンテナを起動します
$ export REPOSITORY_NAME=【リポジトリ名】
$ docker build -t ${REPOSITORY_NAME} .
$ docker run --rm -p 9000:8080 ${REPOSITORY_NAME}
そして、ローカルの9000番ポートを叩くと、下記のようにS3が作成されるため、addに1が入った状態でかえってきました
$ curl -sd '{}' http://localhost:9000/2015-03-31/functions/function/invocations | jq .
{
"add": 1,
"change": 0,
"destroy": 0
}
ECRへアップ
上記で設定した REPOSITORY_NAME
の名前でECRのリポジトリを作成します
その後、下記でイメージをプッシュすると、ECRへイメージがlatestで上がっています
$ 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結果が帰ってきました
これを利用すれば、外部からこのAPIを叩いて、現状のリソースとの差分がないかを監視することが出来ます
さいごに
Lambdaがコンテナで実行できるようになったので、Terraformを入れて実行してみました
今後は、API Gatewayと連携してAPI化して、Terraformの監視をしたり、planだけじゃなくapplyを実行するアプリも作っていきたいですね