CircleCI と serverless を使いブランチごとに Lambda/API Gateway 環境を構築する

More than 1 year has passed since last update.

ブランチごとに確認環境がほしかったので、作っていく。

前提として、



  • serverless-express を使ったWebアプリ。

  • ドメインについては Route53 により管理されている。

という状態で説明する。


プラグインの利用

プラグインを2つ使う


  • serverless-domain-manager: 環境ごとに独自のドメインを割り当てることができる。

  • serverless-kms-secrets: OAuth の secret など、機密情報を暗号化するためにKMSの利用をする。このプラグインはKMSの暗号化を便利に扱うことができる。機密情報がないのであれば、不要。

yarn add -D serverless-domain-manager serverless-kms-secrets


serverless.yml の調整


  • ブランチごとに、S3のバケットがあると鬱陶しいので、deploymentBucket を指定する。

  • 平文の利用が可能な環境変数については、 config/sls-config.yml に設定を外出する。このファイルは環境ごとに、用意しておきS3にアップロードし、deploy時にダウンロードして使うようにする。(後述)

  • 暗号化済みの環境変数は config/secure.yml に格納する。扱いについては、config/sls-config.yml と同様。


serverless.yml

service:

name: ${self:custom.name}

provider:
name: aws
region: ap-northeast-1
runtime: nodejs6.10
stage: ${opt:stage, 'dev'}
deploymentBucket:
name: ${self:custom.name}.${self:provider.region}.deploy
environment:
XXX: ${self:custom.config.XXX, ''}
YYY: ${self:custom.config.YYY, ''}
SECURE_ZZZ: ${self:custom.kmsSecrets.secrets.ZZZ}
iamRoleStatements:
- Effect: 'Allow'
Action:
- "KMS:Decrypt"
Resource: ${self:custom.kmsSecrets.keyArn}

custom:
name: project-name
serverless-kms-secrets:
secretsFile: config/secure.yml
customDomain:
domainName: ${self:provider.stage}.sls.example.com
basePath: ''
stage: ${self:provider.stage}
certificateName: '*.sls.example.com'
createRoute53Record: true
config: ${file(./config/sls-config.yml)}
kmsSecrets: ${file(./config/secure.yml)}

plugins:
- serverless-domain-manager
- serverless-kms-secrets

functions:
render:
handler: server.render
events:
- http:
path: '/'
method: 'get'
private: false
- http:
path: '{proxy+}'
method: 'get'
private: false



一部環境変数の暗号化

Lambda の環境変数は、機密なものであればKMSを利用して暗号化することを推奨している。そこで、serverless-kms-secrets を利用して暗号化を行う。

予めKMSで暗号化キーを作成しておく。

config/secure.yml を作成し、以下のようなファイルを作る


config/secure.yml

keyArn: 'arn:aws:kms:ap-northeast-1:xxxx:key/xxxxxxxxxxxx'

secrets: {}

ZZZ という環境変数を暗号化する場合は、

sls encrypt --name="ZZZ" --value="value"

のようにして暗号化を行うことが出来る。

暗号化された情報は、config/secure.yml (serverless.ymlcustom.serverless-kms-secrets.secretsFile で設定されたファイル) に格納される。serverless.yml では、暗号化された config/secure.yml 内の情報を、環境変数として利用する。

利用時は、以下のように情報を復号化して使う。

import { KMS } from 'aws-sdk'

const kms = new KMS()

const encryptedBuf = Buffer.from(process.env['SECURE_ZZZ'], 'base64')
const cipherText = {CiphertextBlob: encryptedBuf}
kms.decrypt(cipherText).promise().then((data) => {
// 値の取り出し
const value = data.Plaintext.toString('ascii')
})


環境ごとに設定ファイルを準備する

デプロイの戦略を以下とする。


  • master branch については、production 用の設定を取り出す

  • develop branch については staging 用の設定を取り出す

  • その他の branch については、 preview 用の設定を取り出す

ということで、上記3つの環境ごとに設定ファイル (sls-config.yml, secure.yml) を用意して、環境名ごとにprefixを分けてS3 Bucketに格納しておく。


デプロイ用スクリプトの作成

デプロイに使うスクリプトを用意する。


  • ステージ名には -/_ あたりの文字が入ると厄介なので (API Gateway, Lambda, CloudFormation あたりのどれかでいずれかの文字を受け付けない) 除去したものを利用する。 123-test-branch とかであれば、 123testbranch が stage 名となる。

  • パーミッションは755にしておく。


scripts/deploy.sh

#!/bin/bash

set -eu

export NODE_ENV="preview"

if [ "${CIRCLE_BRANCH}" == "master" ]; then
export NODE_ENV="production"
elif [ "${CIRCLE_BRANCH}" == "develop" ]; then
export NODE_ENV="staging"
fi

STAGE=${CIRCLE_BRANCH//[-\/]/}
echo stage=${STAGE}

# get config file from S3
aws s3 cp s3://xxx/${NODE_ENV}/sls-config.yml config/
aws s3 cp s3://xxx/${NODE_ENV}/secure.yml config/

# make Route53 configuration
./node_modules/.bin/sls create_domain --stage=$STAGE
# deploy
./node_modules/.bin/sls deploy --stage=$STAGE
# remove old lambda functions
./scripts/remove_old_stage.sh


上のスクリプトで、デプロイを行うと、どんどん関数が増えちゃうので、リモートブランチを消したら、次のデプロイのタイミングで環境も消すスクリプトを作っておく。


scripts/remove_old_stage.sh

#!/bin/bash

set -eu

STACKS=`aws cloudformation list-stacks --stack-status-filter CREATE_COMPLETE CREATE_FAILED UPDATE_COMPLETE ROLLBACK_COMPLETE ROLLBACK_FAILED UPDATE_ROLLBACK_FAILED --max-items 1000 | jq '.StackSummaries[].StackName' -r | grep '^project-name-'`
BRANCHES=`git branch -r | sed -e "s/^[[:space:]]*origin\///" | grep -v "^HEAD" | uniq`

echo "stacks----"
echo "${STACKS}"
echo "branches----"
echo "${BRANCHES}"

while read line
do
found=0

lineStage=`echo $line | sed -e 's/^project-name-//'`

while read branch
do
stage=${branch//[-\/]/}

if [[ "$lineStage" = "$stage" ]]; then
found=1
break;
fi
done
<<END
$BRANCHES
END

if [ $found -eq 0 ]; then
./node_modules/.bin/sls remove --stage=$lineStage || true
./node_modules/.bin/sls delete_domain --stage=$lineStage || true
fi

done <<END
$STACKS
END



.circleci/config.yml の調整


docker image を用意する

以下が動作する docker image を作っておく


  • nodejs6

  • python

  • pip

  • jq

  • awscli


設定


.circleci/config.yml

version: 2

jobs:
build:
docker:
- image: my_docker_image
parallelism: 1
working_directory: ~/project-name
steps:
- checkout

- restore_cache:
key: project-name-yarn-{{ checksum "yarn.lock" }}

- run:
name: Install node modules
command: yarn install

- run:
name: lint
command: yarn run lint

- run:
name: unit
command: yarn run unit

- save_cache:
key: project-name-yarn-{{ checksum "yarn.lock" }}
paths:
- ~/.yarn-cache

- run:
name: deploy
command: scripts/deploy.sh



完了

これで、各ブランチごとに stage名.sls.example.com が展開される。

serverless-domain-manager による環境の展開については、Route53、CloudFront の設定に時間がかかるため、初回展開時には40分ほどリクエストできるようになるまで時間がかかるとのこと。