CircleCI
lambda
APIGateway
serverless

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

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

前提として、

  • 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分ほどリクエストできるようになるまで時間がかかるとのこと。