CircleCI と GitHub で AWS SAM のサーバーレスアプリを自動デプロイしてみた (開発環境 & 本番環境)

CircleCIを使って、AWS SAMのデプロイを自動化してみました。

やったことをまとめた結果、ハンズオンみたいな手順書になりました。

なお、デプロイのみを対象とします。(テストは扱いません)

おすすめの方

  • AWS SAMを使っている
  • AWS SAMを自動デプロイしたい
  • CircleCIで自動デプロイしたい

目次

環境

項目 バージョン
macOS High Mojave 10.14.5
AWS CLI aws-cli/1.16.179 Python/3.6.8 Darwin/18.6.0 botocore/1.12.169
AWS SAM CLI 0.17.0
Python 3.6
リポジトリ GitHub

リポジトリとブランチ運用

Gitリポジトリは、GitHubフローのような運用とし、Pushされたタグを本番環境にデプロイします。

自動デプロイの対象は、下記とします。デプロイ先は、同じAWSアカウントです。

種類 名前の例 環境 AWSスタック名
ブランチ master 開発環境 CircleCIDeploySample-dev
ブランチ issues/123 開発環境 CircleCIDeploySample-dev
タグ v1.2.3 本番環境 CircleCIDeploySample-prod

AWSにサーバーレスアプリケーションを作成する

作るものは単純なWebAPIです

次のWebAPIを作成します。

Path Method
/message/{id} GET

下記のようなJSONを返します。

{
    "message": "Pathで指定したid"
}

AWS SAMプロジェクトの準備

下記コマンドでプロジェクト一式を作成します。

$ sam init --runtime python3.6 --name CircleCIDeploySample

Python仮想環境の構築

pyenvとpipenvの導入

導入済みの場合は、次へどうぞ!

pyenvを導入します。

$ brew install pyenv
$ echo 'eval "$(pyenv init -)"' >> ~/.bash_profile
$ exec $SHELL -l

pipenvを導入します。

$ brew install pipenv
$ echo 'eval "$(pipenv --completion)"' >> ~/.bash_profile
$ exec $SHELL -l

Python仮想環境の作成

Pythonをインストールします。すでにある場合は次へ。

$ pyenv install 3.6.8

zipimport.ZipImportError: can't decompress data; zlib not availableなエラーが出た場合は、下記をご覧ください。

続いて、仮想環境を作成します。

$ pipenv install --python 3.6.8

仮想環境に入ります。

$ pipenv shell

なお、仮想環境はexitで終了できます。

必要なライブラリを仮想環境に導入

仮想環境に入った状態で、ライブラリを導入します。

$ pipenv install awscli
$ pipenv install aws-sam-cli

SwaggerでAPI Gatewayを定義

swagger.yamlを作成し、下記とします。

swagger: "2.0"
info:
  description: SwaggerとAPI Gatewayのサンプルです。(CircleCIデプロイ用)
  version: 1.0.0
  title: Swagger Sample for CircleCI deploy
tags:
  - name: Message
schemes:
  - https
paths:
  /message/{id}:
    get:
      tags:
        - Message
      summary: メッセージ取得
      description: メッセージを取得します
      consumes:
        - application/json
      produces:
        - application/json
      parameters:
        - name: id
          in: path
          description: 任意のID
          required: true
          type: string
      responses:
        200:
          description: successful operation
          schema:
            $ref: "#/definitions/MessageResponse"
      x-amazon-apigateway-integration:
        uri:
          Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${HelloWorldFunction.Arn}/invocations
        passthroughBehavior: when_no_templates
        httpMethod: POST
        type: aws_proxy
definitions:
  MessageResponse:
    type: object
    required:
      - message
    properties:
      message:
        type: string

Lambda関数の作成

下記とします。

import json

def lambda_handler(event, context):
    request_id = event['pathParameters']['id']
    return {
        'statusCode': 200,
        'body': json.dumps({
            'message': request_id
        }),
    }

requirements.txtの内容は、空っぽにします。

AWS SAMテンプレートファイルの更新

下記とします。

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: CircleCIDeploySample

Globals:
  Function:
    Timeout: 3

Parameters:
  SystemEnv:
    Type: String
    AllowedValues:
      - prod
      - dev

Resources:
  HelloWorldFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: hello_world/
      Handler: app.lambda_handler
      Runtime: python3.6
      Events:
        HelloWorld:
          Type: Api
          Properties:
            Path: /message/{id}
            Method: get
            RestApiId: !Ref HelloWorldApi

  HelloWorldApi:
    Type: AWS::Serverless::Api
    Properties:
      Name: !Sub CircleCIDeploySample-Api-${SystemEnv}
      StageName: !Sub ${SystemEnv}
      DefinitionBody:
        Fn::Transform:
          Name: AWS::Include
          Parameters:
            Location: !Sub s3://cm-fujii-circleci-deploy-sample-bucket${SystemEnv}/swagger.yaml

Outputs:
  HelloWorldApiUrl:
    Value: !Sub https://${HelloWorldApi}.execute-api.${AWS::Region}.amazonaws.com/${SystemEnv}/message/{id}

AWS SAMデプロイの準備

Makefileの作成

Makefileを作成し、デプロイで使用するコマンドをまとめます。あとで使うコマンド達も記載しています。

BUCKET_NAME := cm-fujii-circleci-deploy-sample-bucket-${SYSTEM_ENV}
STACK_NAME := CircleCIDeploySample-${SYSTEM_ENV}

create-bucket:
	aws s3 mb s3://$(BUCKET_NAME)

copy-swagger:
	aws s3 cp swagger.yaml s3://$(BUCKET_NAME)/swagger.yaml

build:
	sam build

deploy:
	sam package \
	    --output-template-file packaged.yaml \
		--s3-bucket $(BUCKET_NAME)

	sam deploy \
		--template-file packaged.yaml \
		--stack-name $(STACK_NAME) \
		--capabilities CAPABILITY_NAMED_IAM \
		--no-fail-on-empty-changeset \
		--parameter-overrides SystemEnv=${SYSTEM_ENV}

get-apigateway-endpoint:
	aws cloudformation describe-stacks \
		--stack-name $(STACK_NAME) \
		--query 'Stacks[].Outputs'

create-iam-user-for-circleci:
	aws cloudformation deploy \
		--template-file circleci-iam-user.yaml \
		--stack-name ${STACK_NAME}-for-CircleCI-User \
		--capabilities CAPABILITY_NAMED_IAM \
		--parameter-overrides SystemEnv=${SYSTEM_ENV}

create-iam-user-access-key:
	aws iam create-access-key \
		--user-name CircleCIDeploySample-${SYSTEM_ENV}-for-CircleCI-User

delete-app:
	aws cloudformation delete-stack \
		--stack-name $(STACK_NAME)

delete-iam-user-for-ciecleci:
	aws cloudformation delete-stack \
		--stack-name ${STACK_NAME}-for-CircleCI-User

CircleCIの準備

CircleCI用の設定ファイルを作成

.circleciディレクトリを作成し、その中にconfig.ymlを作成します。

$ mkdir .circleci
$ touch .circleci/config.yml

続いて、config.ymlファイルの中身を記述します。

version: 2.1
executors:
  my-executor:
    docker:
      - image: circleci/python:3.6.8
        environment:
          PIPENV_VENV_IN_PROJECT: true
    working_directory: ~/CircleCIDeploySample

commands:
  restore:
    steps:
      - restore_cache:
          key: CircleCIDeploySample-v3-{{ .Branch }}-{{ checksum "Pipfile.lock" }}

  save:
    steps:
      - save_cache:
          paths:
            - ".venv"
          key: CircleCIDeploySample-v3-{{ .Branch }}-{{ checksum "Pipfile.lock" }}

jobs:
  setup:
    executor: my-executor
    steps:
      - checkout
      - restore
      - run:
          name: install
          command: |
            sudo pip install pipenv
            pipenv install
      - save

  build:
    executor: my-executor
    parameters:
      env:
        type: enum
        enum: ["prod", "dev"]
    steps:
      - checkout
      - restore
      - run:
          name: sam-build
          command: |
            source .venv/bin/activate

            aws --version
            sam --version

            echo << parameters.env >>

            if [ << parameters.env >> = "dev" ]; then
              export SYSTEM_ENV=<< parameters.env >>
              export AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID_DEV
              export AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY_DEV
            else
              export SYSTEM_ENV=<< parameters.env >>
              export AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID_PROD
              export AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY_PROD
            fi

            make copy-swagger
            make build

  deploy:
    executor: my-executor
    parameters:
      env:
        type: enum
        enum: ["prod", "dev"]
    steps:
      - checkout
      - restore
      - run:
          name: sam-deploy
          command: |
            source .venv/bin/activate

            aws --version
            sam --version

            echo << parameters.env >>

            if [ << parameters.env >> = "dev" ]; then
              export SYSTEM_ENV=<< parameters.env >>
              export AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID_DEV
              export AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY_DEV
            else
              export SYSTEM_ENV=<< parameters.env >>
              export AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID_PROD
              export AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY_PROD
            fi

            make deploy

workflows:
  version: 2.1
  release-dev-workflow:
    jobs:
      - setup:
          filters:
            branches:
              only:
                - master
                - /^issues\/\d+$/

      - build:
          env: dev
          requires:
            - setup
          filters:
            branches:
              only:
                - master
                - /^issues\/\d+$/

      - deploy:
          env: dev
          requires:
            - build
          filters:
            branches:
              only:
                - master
                - /^issues\/\d+$/

  release-prod-workflow:
    jobs:
      - setup:
          filters:
            branches:
              ignore: /.*/
            tags:
              only:
                - /^v\d+\.\d+\.\d+$/

      - build:
          env: prod
          requires:
            - setup
          filters:
            branches:
              ignore: /.*/
            tags:
              only:
                - /^v\d+\.\d+\.\d+$/

      - deploy:
          env: prod
          requires:
            - build
          filters:
            branches:
              ignore: /.*/
            tags:
              only:
                - /^v\d+\.\d+\.\d+$/

CircleCI用のIAMユーザを作成

Web画面から作成してもよいのですが、せっかくなのでCloudFormationを利用して作成します。

circleci-iam-user.yamlを作成し、下記とします。

AWSTemplateFormatVersion: 2010-09-09
Description: Create IAM User and Role for CircleCI
Parameters:
  SystemEnv:
    Type: String
    Description: SystemEnv
    AllowedValues:
      - prod
      - dev
Resources:
  CircleCIUser:
    Type: AWS::IAM::User
    Properties:
      UserName: !Sub CircleCIDeploySample-${SystemEnv}-for-CircleCI-User
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AmazonS3FullAccess
        - arn:aws:iam::aws:policy/AmazonAPIGatewayAdministrator
        - arn:aws:iam::aws:policy/AWSLambdaFullAccess
        - arn:aws:iam::aws:policy/IAMFullAccess

  CircleCIPoricy:
    Type: AWS::IAM::Policy
    Properties:
      PolicyName: !Sub lCircleCIDeploySample-${SystemEnv}-for-CircleCI-policy
      PolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Action:
              - "cloudformation:*"
            Resource: "*"
      Users:
        - !Ref CircleCIUser
簡略化のため雑に作成しています。よりセキュアに作るためには、必要最低限のポリシーを作成するなど、ベストプラクティス等を参照してください。 IAMのベストプラクティス | AWS

CircleCI用のIAMユーザ作成を実行します。

$ SYSTEM_ENV=dev make create-iam-user-for-circleci
$ SYSTEM_ENV=prod make create-iam-user-for-circleci

アクセスキーIDとシークレットキーの取得

下記コマンドで取得し、メモしておきます。

$ SYSTEM_ENV=dev make create-iam-user-access-key
$ SYSTEM_ENV=prod make create-iam-user-access-key

流出しないよう、取扱にご注意ください!

S3バケットの作成

ソースコードデプロイ先のバケットを作成します。

$ SYSTEM_ENV=dev make create-bucket
$ SYSTEM_ENV=prod make create-bucket

コミット

ここまでの内容をmasterブランチにコミットします。コミット済みの場合は次へ!

$ git add .
$ git commit -m "create sample"

リポジトリのPush

リポジトリをGitHubにPushします。

$ git push origin master

CircleCIの設定

ここから先は、Web画面で行います。

ログイン

CircleCIにログインします。

CircleCIにログインする

CircleCIのプロジェクトを作成

「ADD PROJECT」を選択し、さきほどGitHubにPushしたリポジトリを選択します。

対象プロジェクトを選択する

続いて、「Start building」を選択します。

「Start building」を選択する

初めてのジョブが走りますが、環境変数が未設定なので失敗します。

はじめてのジョブがRunning中

はじめてのジョブが失敗

環境変数の設定

プロジェクト一覧の設定マークを押し、設定画面に移ります。

設定マークを選択する

Environment Variablesを選択します。

環境変数を設定する

次の環境変数を追加します。

Name Value
AWS_ACCESS_KEY_ID_DEV 取得したAccessKeyId(開発用)
AWS_ACCESS_KEY_ID_PROD 取得したAccessKeyId(本番用)
AWS_SECRET_ACCESS_KEY_DEV 取得したSecretAccessKey(開発用)
AWS_SECRET_ACCESS_KEY_PROD 取得したSecretAccessKey(本番用)
AWS_DEFAULT_REGION ap-northeast-1
AWS_DEFAULT_OUTPUT json

CircleCIでデプロイさせる!(開発環境)

開発環境にデプロイ

さきほど失敗したWorkflowsの「Return」を選択し、そこの「Return from failed」を選択します。

「Return from failed」を選択する

すると、再びデプロイが始まります。

再デプロイ中

しばらくすると成功します!!! (ブラウザによっては自動更新してくれないため、手動更新してください)

デプロイに成功!

API Gatewayのアドレスを取得する(開発環境)

$ SYSTEM_ENV=dev make get-apigateway-endpoint
[
    [
        {
            "OutputKey": "HelloWorldApiUrl",
            "OutputValue": "https://yyyyyy.execute-api.ap-northeast-1.amazonaws.com/dev/message/{id}"
        }
    ]
]

APIを叩いてみる(開発環境)

適当に叩きます。

$ curl https://yyyyyy.execute-api.ap-northeast-1.amazonaws.com/dev/message/dev-test-0000
{"message": "dev-test-0000"}

できました!!!

CircleCIでデプロイさせる!(本番環境)

本番用Lambdaを少し修正する

せっかくなので、少しだけLambdaのコードを変えてみます。

import json

def lambda_handler(event, context):
    request_id = event['pathParameters']['id']
    return {
        'statusCode': 200,
        'body': json.dumps({
            'env': 'prod',
            'message': request_id
        }),
    }

特に意味もないですが、prodと固定文字列を追加してみました。

本番環境にデプロイ

まずはコミットします。

$ git add .
$ git commit -m "modify app.py for prod"

続いて、タグを付けて、そのタグをPushします!

$ git tag v1.0.0
$ git push origin v1.0.0

すると、CircleCIによる本番環境へのデプロイが始まります。

本番環境にデプロイ中

しばらくすると、完了しました!

本番環境にデプロイ成功!

API Gatewayのアドレスを取得する(本番環境)

$ SYSTEM_ENV=prod make get-apigateway-endpoint
[
    [
        {
            "OutputKey": "HelloWorldApiUrl",
            "OutputValue": "https://zzzzzz.execute-api.ap-northeast-1.amazonaws.com/prod/message/{id}"
        }
    ]
]

APIを叩いてみる(本番環境)

適当に叩きます。

$ curl https://zzzzzz.execute-api.ap-northeast-1.amazonaws.com/prod/message/hello-world
{"env": "prod", "message": "hello-world"}

できました!!!

さいごに

こういった作業は初めてだったので、とても時間が掛かりましたが、良い勉強になりました。

どんどん活用していきます!

参考