Tech Do | メディアドゥの技術ブログ

株式会社メディアドゥのエンジニアによるブログです。

GitHub ActionsとServerless FrameworkでLambdaに自動デプロイを仕込む

f:id:qazx7412:20200815183451j:plain

こんにちは、追いかけているRPGのシリーズの最新作の発売日が迫っていて、日々そわそわしながら過ごしているエンジニアの回路(@qazx7412)です。

私はいろんな言語でLambdaを使ってslack botを作るのが趣味なのですが、このbot達のリポジトリにGitHub Actionsで自動デプロイを仕込んだので今回はその話をします。

この記事は、以前Tech Do Book #2で解説をしたCodePipelineとCodeBuildを利用した自動デプロイの解説の続編になります。
読んでいなくとも問題ないようにしていますが是非こちらもよろしくお願いいたします。

GitHub Actionsの使い方

ということでまずはGitHub Actionsの使い方を説明します。
一見難しそうですがGitHub Actionsを使うのは簡単です。
リポジトリ内の .github/workflows/ に下記のような振る舞いを定義したymlファイルを設置するだけです。

# .github/workflows/sample.yml

name: workflow-name

on:
  push:
    branches: [ master ]

jobs:
  deploy:
    name: deploy
    runs-on: ubuntu-latest
    steps:

    - name: sample run
      run: echo "何でもできる"

    - name: sample uses
      uses: actions/setup-go@v2
      with:
        go-version: ^1.13
      id: go

上から見ていくと、まず on でいつ実行するかを定義しています。 この例だとmasterブランチにpushが行われたとき実行します。
次に jobs で何をするかを定義しています。
サンプルではコマンドを直接実行したり、公開されているアクションを呼び出したりしています。
(詳しい構文の詳細はこちらを参照してください。)

Serverless Frameworkを利用した自動デプロイ

ここから、Serverless Framework(以下serverless)を利用したLambdaの自動デプロイの仕込み方を紹介します。
他のデプロイツールを利用したい場合は、適宜読み替えてください。
今回も例のごとく言語はGoです。

# .github/workflows/go-sls.yml

name: go-sls

on:
  push:
    branches: [ master ]

jobs:
  deploy:
    name: deploy
    runs-on: ubuntu-latest
    steps:

    - name: set up
      uses: actions/setup-go@v2
      with:
        go-version: ^1.13
      id: go

    - name: checkout
      uses: actions/checkout@v1

    - name: build
      run: go build -ldflags="-s -w" -o bin/handler handler/main.go

    - name: configure aws
      uses: aws-actions/configure-aws-credentials@v1
      with:
        aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
        aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        aws-region: ap-northeast-1

    - name: get env
      run: |
        aws s3 cp s3://path/to/env.yml .

    - name: deploy
      uses: serverless/github-action@master
      with:
        args: deploy --verbose --stage prod
      env:
        AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
        AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

内容を詳しく見ていきましょう。

ビルド

    - name: set up
      uses: actions/setup-go@v2
      with:
        go-version: ^1.13
      id: go

    - name: checkout
      uses: actions/checkout@v1

    - name: build
      run: go build -ldflags="-s -w" -o bin/handler handler/main.go

最初にGoをビルドします。
まず actions/setup-go を利用してGoのビルドのための環境をセットアップします。
Goはセットアップ用のアクションが公式で用意されているので、楽にセットアップできます。
次に、 actions/checkout を利用して、ビルドしたいブランチ(今回の場合はmaster)にチェックアウトすることで、ビルドをできるようにします。
準備が完了したら手動でのビルドと同じように go build を実行してビルドを行います。

    - name: checkout
      uses: actions/checkout@v1

    - name: build
      id: sbt
      uses: lokkju/github-action-sbt@11-1.3.0-2.13.0
      with:
        commands: assembly
        sbt_project_directory: ./

Goはセットアップをするためのオプションが用意されていましたが、他の言語の場合ビルドコマンドやパッケージマネージャーが呼び出せるアクションが用意されている場合もあります。
上記の例はScalaのパッケージマネージャーのsbtを利用するアクションです。
これで sbt assembly と同等のコマンドを実行できます。

  deploy:
    name: deploy
    runs-on: ubuntu-latest
    container:
      image: google/dart:latest
    - name: checkout
      uses: actions/checkout@v1

    - name: pub get
      run: pub get

    - name: build
      run: dart2native ./src/main.dart -o ./bootstrap

また、アクションが存在しない場合は、コンテナを利用してビルドを行うことができます。 上記はDartの場合の例なのですが、公式で用意されているコンテナを利用してビルドをしています。

デプロイ

    - name: configure aws
      uses: aws-actions/configure-aws-credentials@v1
      with:
        aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
        aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        aws-region: ap-northeast-1

    - name: get env
      run: |
        aws s3 cp s3://path/to/env.yml .

次にAWS CLIをセットアップして、S3からなにやら怪しげなymlを落として来ています。
まずAWS CLIですが、 aws-actions/configure-aws-credentials という AWS CLIの設定を行うことができるアクションがあるのでこれを利用しています。
ここでアクションに ${{ secrets.HOGE }} という変数らしきものを引き渡していますが、これは secrets に入っているクレデンシャルをアクションへ引き渡しています。
これはリポジトリのページの Setting -> Secrets から設定ができます。

f:id:qazx7412:20200815184334p:plain

そして、 env.yml って何?という話ですが、これはserverlessの設定を定義する serverless.yml から環境変数の設定を切り出したものです。 serverlessでは下記のように記述することでymlを分割することができます。

# serverless.yml

provider:
  name: aws
  runtime: go1.x
  stage: ${opt:stage, self:custom.defaultStage}
  region: ap-northeast-1
  environment:
    ${file(./env.yml)}

私がserverlessを利用するときは、上記で環境変数に関する部分を切り出して、gitignoreするように運用しています。
なので今回の場合はS3に適当に切り出したymlを置いておいて、デプロイ時にダウンロードするようにしました。

    - name: deploy
      uses: serverless/github-action@master
      with:
        args: deploy --verbose --stage prod
      env:
        AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
        AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

最後にデプロイです。
serverlessでは公式でコマンドを利用できるアクションが用意されているのでそれを利用します。
こういうものが用意されていると特に変な小細工とかをする必要がないので楽ですね。

    - name: setup node.js
      uses: actions/setup-node@v1

    - name: install sls
      run: npm i -g serverless

    - name: deploy
      run: sls deploy -s prod > ./result.txt

    - name: push result
      run: |
        aws s3 cp ./result.txt s3://path/to/result.txt

また、もし公式のアクションではできないような小細工をしたいなどで既存のアクションを使わない場合は上記のように直接インストールして実行することもできます。
この例では、serverlessの標準出力をテキストファイルに出すようにして、そのままS3へ送っています。
これはserverlessではデプロイされるLambda関数にweb APIが含まれているとURLを標準出力に出してしまい、それをGitHub Actionsの場合誰でも閲覧可能になってしまうのでこういった小細工をするようにしています。

あとがき

ということで簡単ではありましたがGitHub Actionsを利用したLambdaの自動デプロイの仕込み方を紹介しました。
今回はデプロイにフォーカスして紹介しましたが、他にもPRをトリガーにして自動テストを動かすとかいろいろ使いどころがありそうでいいですね。
パブリックリポジトリならば無料で使えるので個人開発などで気軽に試せるのもいいです。
弊社ではデプロイを自動化したいエンジニアを絶賛募集中ですので、興味がありましたらぜひこちらからお願いいたします。

www.wantedly.com

参考

付録

自分用の備忘録を兼ねて各言語のymlの例を載せて置きますので参考にしてください。

Scala

name: scala-sls

on:
  push:
    branches: [ master ]

jobs:
  deploy:
    name: deploy
    runs-on: ubuntu-latest

    steps:

    - name: checkout
      uses: actions/checkout@v1

    - name: build
      id: sbt
      uses: lokkju/github-action-sbt@11-1.3.0-2.13.0
      with:
        commands: assembly
        sbt_project_directory: ./

    - name: configure aws
      uses: aws-actions/configure-aws-credentials@v1
      with:
        aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
        aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        aws-region: ap-northeast-1

    - name: get env
      run: |
        aws s3 cp s3://path/to/env.yml .

    - name: deploy
      uses: serverless/github-action@master
      with:
        args: deploy --verbose --stage prod
      env:
        AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
        AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

Dart

name: dart-sls

on:
  push:
    branches: [ master ]

jobs:
  deploy:
    name: deploy
    runs-on: ubuntu-latest
    container:
      image: google/dart:latest

    steps:

    - name: checkout
      uses: actions/checkout@v1

    - name: pub get
      run: pub get

    - name: build
      run: dart2native ./src/main.dart -o ./bootstrap

    - name: install aws
      run: apt-get -y update && apt-get -y install awscli

    - name: configure aws
      uses: aws-actions/configure-aws-credentials@v1
      with:
        aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
        aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        aws-region: ap-northeast-1

    - name: get env
      run: |
        aws s3 cp s3://path/to/env.yml .

    - name: deploy
      uses: serverless/github-action@master
      with:
        args: deploy --verbose --stage prod
      env:
        AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
        AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

Nim

name: nim-sls

on:
  push:
    branches: [ master ]

jobs:
  deploy:
    name: deploy
    runs-on: ubuntu-latest
    container:
      image: nimlang/nim:1.0.6

    steps:

    - name: checkout
      uses: actions/checkout@v1

    - name: build
      run: nimble build -d:ssl && mv main bootstrap && chmod +x bootstrap

    - name: install aws
      run: apt-get -y update && apt-get -y install awscli
      env:
        DEBIAN_FRONTEND: noninteractive

    - name: configure aws
      uses: aws-actions/configure-aws-credentials@v1
      with:
        aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
        aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        aws-region: ap-northeast-1

    - name: get env
      run: |
        aws s3 cp s3://path/to/env.yml .

    - name: deploy
      uses: serverless/github-action@master
      with:
        args: deploy --verbose --stage prod
      env:
        AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
        AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

Crystal

name: crystal-sls

on:
  push:
    branches: [ master ]

jobs:
  deploy:
    name: deploy
    runs-on: ubuntu-latest
    container:
      image: crystallang/crystal

    steps:

    - name: checkout
      uses: actions/checkout@v1

    - name: shards install
      run: shards install

    - name: build
      run: shards build --static && mv bin/* . && chmod +x bootstrap

    - name: install aws
      run: apt-get -y update && apt-get -y install awscli

    - name: configure aws
      uses: aws-actions/configure-aws-credentials@v1
      with:
        aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
        aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        aws-region: ap-northeast-1

    - name: get env
      run: |
        aws s3 cp s3://path/to/env.yml .

    - name: deploy
      uses: serverless/github-action@master
      with:
        args: deploy -s dev
      env:
        AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
        AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}