Lambda Layerを使ったLambda FunctionをGitLab-CIでデプロイする

インフラの奥村です。

とある運用ツールをLambda化した際に、GitLab-CIとTerraformとLambrollを利用してCI/CDの実装を行ったのでその時の知見を共有します。
特に難しい事はしていないですが、Lambda layerを利用したLambdaのGitLab-CIの実装の一例として参考にして頂ければ幸いです。

当記事では、CDについて詳しく記載しております。実際にはCIとしてユニットテストなどを設定するのが良いと思います。

背景

  • Lambda関数に更新があった場合、Terraformのリソースと関係なくデプロイしたい。Lambda関数のデプロイにはLambrollを用いる。
  • Lambda関数のデプロイのたびにzipモジュールをアップロードするのには時間が掛かるのでレイヤーを利用する。
  • レイヤーを利用すれば、Lambda関数が新たに作成されたときに共通のモジュール参照先として利用できる。

使用するもの

  • Lambda
    • Python 3.7
  • Docker Image
    • Lambroll
      • 0.3.1
    • Terraform
      • 0.12.28
  • GitLab-CI
  • Bashスクリプト

各リソースの更新フロー

基本的に GitLab-Flow でデプロイできるようにしています。

Lambda関数の更新

  1. ローカルでPythonファイルを更新する
  2. GitLabへPush
  3. Gitlab-ciによるunittest(当記事ではスキップ)
  4. masterブランチへマージ
  5. masterブランチからproductionブランチへマージリクエスト&マージ
  6. GitLab-CIより手動実行用のジョブが作成される
  7. ジョブを手動実行
    1. Lambrollによるデプロイ

レイヤーの更新(requirements.txtの更新)

  1. ローカルで requirements.txt ファイルを更新する
  2. GitLabへPush
  3. masterブランチへマージ
  4. GitLab-CIによるジョブの実行
    1. module_setup_for_terraform.sh の実行
    2. terraform_deploy_user.sh の実行
      1. terraform planの実行
  5. masterブランチからproductionブランチへマージリクエスト&マージ
  6. GitLab-CIにより手動実行用のジョブが作成される
  7. ジョブを手動実行
    1. module_setup_for_terraform.sh の実行
    2. terraform_deploy_user.sh の実行
      1. terraform applyの実行

Terraformの更新

  1. ローカルで .tf ファイルを更新する
  2. GitLabへPush
  3. masterブランチへマージ
  4. GitLab-CIによるジョブの実行
    1. module_setup_for_terraform.sh の実行
    2. terraform_deploy_user.sh の実行
      1. terraform planの実行
  5. masterブランチからproductionブランチへマージリクエスト&マージ
  6. GitLab-CIにより手動実行用のジョブが作成される
  7. ジョブを手動実行
    1. module_setup_for_terraform.sh の実行
    2. terraform_deploy_user.sh の実行
      1. terraform applyの実行

成果物

ディレクトリ構成

├── sample-lambda-function
│   ├── __init__.py
│   ├── lambda_function.py
│   ├── production.json
│   ├── README.md
│   └── requirements.txt
├── terraform
│   └── production
│       ├── deploy_user.tfvars
│       ├── lambda.tf
│       ├── provide.tf
│       └── vars.tf
└── tool_scripts
    ├── module_setup_for_terraform.sh
    ├── set_aws_config.sh
    └── terraform_deploy_user.sh

Docker Image

TerraformとLambrollのバージョンは私の環境で動作していたバージョンを指定しています。

ビルドしたイメージをGitLab-CIでPullできるレジストリにPushします。(私の部署では GitLab Registryを利用しています。)

FROM alpine AS builder

RUN apk update && apk add curl tar && \
    curl -sSL https://github.com/fujiwara/lambroll/releases/download/v0.3.1/lambroll_v0.3.1_linux_amd64.tar.gz > lambroll_v0.3.1_linux_amd64.tar.gz && \
    tar xf lambroll_v0.3.1_linux_amd64.tar.gz

FROM python:3.7

ENV TERRAFORM_VERSION=0.12.28

COPY --from=builder /lambroll_v0.3.1_linux_amd64/lambroll /usr/local/bin/

RUN apt update && apt install zip  && \
    wget https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/terraform_${TERRAFORM_VERSION}_linux_amd64.zip && \
    unzip terraform_${TERRAFORM_VERSION}_linux_amd64.zip -d /bin && \
    rm -f terraform_${TERRAFORM_VERSION}_linux_amd64.zip && \
    python -m pip install --upgrade pip && \
    python -m pip install awscli boto boto3

.gitlab-ci.yml

Lambda関数が複数作成された場合に別ジョブを簡単に定義できるように、共通化できる部分にはアンカーを用いています。

また、当記事ではCIに関する記述は記載していません。

stages:
  - pre_deploy
  - terraform_deploy
  - lambda_deploy

.template_deploy_python_lambda: &template_deploy_python_lambda
  stage: lambda_deploy
  image: lambroll-terraform
  script:
    - cd $DIRECTORY
    - lambroll deploy --function="${ENVIRONMENT}.json"

deploy_sample_lambda_function_production:
  <<: *template_deploy_python_lambda
  variables:
    DIRECTORY: sample-lambda-function 
    ENVIRONMENT: production
  only:
    refs:
      - production
    changes:
      - sample-lambda-function/**/*
  when: manual

.template_terraform_production: &template_terraform_production
  image: lambroll-terraform
  before_script:
    - bash tool_scripts/set_aws_config.sh
    - bash tool_scripts/module_setup_for_terraform.sh sample-lambda-function  production
    - cd terraform/production
    - terraform init -backend-config deploy_user.tfvars
    - cd ../../

terraform_plan_production:
  stage: pre_deploy
  <<: *template_terraform_production
  script:
    - bash tool_scripts/terraform_deploy_user.sh plan production
  only:
    refs:
      - master
    changes:
      - terraform/**/*
      - sample-lambda-function/requirements.txt

terraform_apply_production:
  stage: terraform_deploy
  <<: *template_terraform_production
  script:
    - bash tool_scripts/terraform_deploy_user.sh apply production
  only:
    refs:
      - production
    changes:
      - terraform/**/*
      - sample-lambda-function/requirements.txt
  when: manual

Lambroll

Lambda関数に関するリソースは sample-lambda-function にまとめられております。
Lambda関数本体は今回の記事の対象外なので記載しておりません。

sample-lambda-function に含める Lambrollの設定ファイルであるJSONファイルは当記事の例では production.json としております。

{
  "FunctionName": "sample-lambda-function",
  "Handler": "lambda_function.lambda_handler",
  "MemorySize": 128,
  "Role": "arn:aws:iam::xxxxxxxxxxxx:role/blog-sample-okumura-role",
  "Runtime": "python3.7",
  "Layers": ["arn:aws:lambda:ap-northeast-1:xxxxxxxxxxxx:layer:sample-lambda-function-layer:1"],
  "Tags" : {
    "environment" : "production"
  }
}

Terraform

terraform/production/deploy_user.tfvars

デプロイスクリプトで指定するデプロイユーザーを指定するtfvarsを用意します。

profile  = "your_profile"
role_arn = ""

terraform/production/lambda.tf

filename で指定しているzipファイルは、GitLab-CIによってデプロイスクリプトの中で生成されます。

resource "aws_lambda_layer_version" "lambda_layer" {
  filename   = "${var.name}.zip"
  layer_name = "${var.name}-layer"

  compatible_runtimes = ["python3.7"]
}

terraform/production/vars.tf

name変数をディレクトリ名と一致させる必要があります。

variable profile {}
variable role_arn {}

variable name {
  default = "sample-lambda-function"
}

デプロイ用Bashスクリプト

tool_scripts/set_aws_config.sh

AWS関連の設定をGitLab-CIのコンテナ内に生成するスクリプトです。
GitLab-CIによって実行されます。

#!/bin/bash

mkdir ~/.aws
echo "[profile infra_aws_deploy]" >> ~/.aws/config
echo "region = ap-northeast-1" >> ~/.aws/config
echo "aws_access_key_id = $AWS_ACCESS_KEY_ID" >> ~/.aws/config
echo "aws_secret_access_key = $AWS_SECRET_ACCESS_KEY" >> ~/.aws/config

tool_scripts/module_setup_for_terraform.sh

Lambda layerをTerraformでplanもしくはapplyするので、Terraformのコマンドを実行するまえに、Layer用のzipファイルを生成する必要があります。
引数でLambda関数名と、環境を指定します。
当スクリプトは、GitLab-CIのTerraformコマンドの前に実行されます。

#!/bin/bash

set -u

lambda_dir=$1
environment=$2

python -m pip install -r "${lambda_dir}/requirements.txt" -t ./python
zip -r "${lambda_dir}.zip" python
mv "${lambda_dir}.zip" terraform/"${environment}"
rm -rf ./python

tool_scripts/terraform_deploy_user.sh

Terraformを実行するラッパースクリプトです。
ローカル環境でも実行しやすいようになっています。

#!/bin/bash

function plan(){
  echo "plan"
  cd terraform/"$1"
  terraform plan -var-file deploy_user.tfvars
}
function apply(){
  echo "apply"
  cd terraform/"$1"
  terraform apply -var-file deploy_user.tfvars -auto-approve
}

set -u
environment=$2

$1 "${environment}"

まとめ

Lambdaのデプロイで何のソリューションを用いるかは議題に上がる事が多々あると思います。

当記事が方法の悩んでいる人の少しの助けにでもなれば幸いです。


 

[お知らせ]
アドウェイズエンジニアブログ公式Twitterアカウントの運用を開始しました。
ぜひフォローお願いします! @ADWAYS_ENGINEER