LoginSignup
0
1

More than 3 years have passed since last update.

Lambda on Terraform Container

Last updated at Posted at 2020-12-15

はじめに

この記事はterraform Advent Calendar 2020の15日目です。

もともとTerraformのテストについてつらつら書こうとしましたが、re:Invent2020でLambdaのコンテナサポートされました
今回re:Invent2020で発表された中で、比較的反響がでかかった発表じゃないでしょうか
この波に乗らねば、ということで、Terraformをコンテナ化してLambdaで実行できるようにしてみました
※決してLambdaをTerraformで構築する記事ではありません

構成

今回の構成は下記です
lambda_terraform.png

開発では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に乗っける場合、下記のどちらかで構築する必要があります

ランタイム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結果が帰ってきました
スクリーンショット 2020-12-15 18.50.57.png
これを利用すれば、外部からこのAPIを叩いて、現状のリソースとの差分がないかを監視することが出来ます

さいごに

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

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1