circleciにlocalstackをterraformで構築する


紹介する内容

  • circleciにlocalstackをterraformで構築します
    • terraformでlambdaを作ります
    • ユーザーが指定したディレクトリにpython packageをインストールします
    • インストールしたpackageをzipに固めます
  • circleciをlocalで動かしてますが、本物のcircleciでも動きます
  • circleciのdocker executorを使います

結論

  • localstackのdocker-entrypoint-initaws.dが使用できないから、
    • docker-entrypoint-initaws.dの作業内容をPrimaryコンテナに書きます、
    • localstack固有のもろもろ初期化が終わるタイミングを把握できません。愚直に初期化作業に掛かる時間を計測して、その時間分待つようにすることが最善です。
  • localstackコンテナの環境変数HOSTNAME_EXTERNALlocalhostにして、Primaryコンテナからのlocalstackアクセスできるようにします。

circleciのdocker executorはvolumnsが使えない

  • circleciのdocker executorはdocker composeのvolumesを使用できません。
    • これがlocalstack固有のもろもろ初期化が終わるタイミングを把握不可能する原因です。volumesが使えないから、docker-entrypoint-initaws.dに介入が不可能です
      • localstackのportが開くことを基準にするのは意味がありません。なぜなら、portを開けたままもろもろ初期化作業するからです
    • https://circleci.com/docs/2.0/executor-types/#docker-benefits-and-limitations
    • どうしてもdocker composeのvolumesを使う必要があったら、machine executorにしたら実装可能だと思います

circleciのdocker executorはネットワークのhost名がlocalhostになっています

  • circleciのdocker executorは一つのネットワーク(common network)がありまして、host名はlocalhostです。Primaryコンテナ含むすべてのコンテナが属してます

(参考)Primaryコンテナ以外のコンテナはできることが限られている印象です

紹介始めます

localstack紹介

実のAWSを使わずにいろいろ試せるものです。

バージョン0.11.0から大きな変化がありました。本記事は0.11.0バージョンのlocalstackを使います

https://github.com/localstack/localstack

terraform紹介

コードでインフラを管理できます。
https://www.terraform.io/

circleci紹介

コードの継続的な統合とテストを助けてくれるものです。
https://circleci.com/

構築実験環境

Linux

  • CentOS Linux release 7.7.1908 (Core)

Docker Compose

  • VERSION : 1.24.1

Docker

  • VERSION : 19.03.5

local circleciをインストールします

ディレクトリ構成

全体ソースコードは https://github.com/cheekykorkind/circleci-localstack で確認できます

  • 全体図

  • terraformファイルを置く部分

  • circleci config部分

当記事のプロジェクトはdocker composeでlocalstackを作ってterraformでlambdaを作る プロジェクトからcircleci設定を追加して調整したバージョンです。docker composeでlocalstackを作る詳しい内容はこちらの記事参考お願いします。
https://qiita.com/cheekykorkind/items/02c896465ddea5c5186f

circleciのconfig.ymlで構築します

jobの名前はtestです。my-testなどでも設定可能です

version: 2
jobs:
  test:
    docker:
      - image: circleci/python:3.7-buster-node
        environment:
          AWS_ACCESS_KEY_ID: AKIAIOSFODNN7EXAMPLE
          AWS_SECRET_ACCESS_KEY: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
          AWS_DEFAULT_REGION: us-east-1

          LAMBDA_ZIP_PATH: /home/circleci/lambda.zip
          HELLO_WORLD_PATH: /home/circleci/project/sam-lambda/hello_world
          TERRAFORM_PATH: /home/circleci/project/terraform

          COMMON_NETWORK: localhost

      - image: localstack/localstack:0.11.0
        environment:
          AWS_ACCESS_KEY_ID: AKIAIOSFODNN7EXAMPLE
          AWS_SECRET_ACCESS_KEY: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
          AWS_DEFAULT_REGION: us-east-1
          SERVICES: lambda,logs
          DEBUG: 1

          HOSTNAME_EXTERNAL: localhost

    steps:
      - checkout

      - run:
          name: Wait for localstack
          command: dockerize -wait tcp://localhost:4566 -timeout 2m

      - run:
          name: install pip packages at packages directory
          command: |
            pip -V
            cd $HELLO_WORLD_PATH
            pip install -t ./packages -r requirements.txt
            zip -r9 $LAMBDA_ZIP_PATH .

      - run:
          name: install terraform 0.12.6
          command: |
            cd ~
            sudo apt-get install unzip
            wget https://releases.hashicorp.com/terraform/0.12.6/terraform_0.12.6_linux_amd64.zip
            sudo unzip -o terraform_0.12.6_linux_amd64.zip
            sudo mv terraform /usr/local/bin/
            terraform --version

      - run:
          name: terraform apply
          command: |
            cd $TERRAFORM_PATH
            terraform init
            terraform apply -auto-approve -var="lambda_zip_path=$LAMBDA_ZIP_PATH" -var="endpoint_domain=$COMMON_NETWORK"

      - run:
          name: install aws cli
          command: |
            sudo apt install awscli

      - run:
          name: test lambda
          command: |
            aws lambda invoke --region us-east-1 \
              --endpoint-url http://localhost:4566 \
              --function-name hello_lambda \
              --payload '{ "name": "Bob" }' \
              response.json

コンテナの環境変数設定

  • circleci/python:3.7-buster-nodeコンテナ(Primaryコンテナ)

    • COMMON_NETWORK はterraformにcircleciのdocker executorのデフォルトhost名localhostを伝えるために作りました
    • AWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEYAWS_DEFAULT_REGION はaws cliを便利に使うために設定しました
    • LAMBDA_ZIP_PATH はlambda生成に使うzipファイルの位置を書きました。コンテナ2つが連携して作業するから管理しやすくするために設定しました
    • HELLO_WORLD_PATH は便利にSAMプロジェクトのpythonコードのzip圧縮をするために設定しました
    • TERRAFORM_PATH はterraformファイルのパスです。terraformコマンド実行するためにterraformファイルがいるところに移動する必要があります。便利な移動のために事前に指定しておきました
  • localstack/localstack:0.11.0コンテナ

    • SERVICESDEBUGHOSTNAME_EXTERNAL はlocalstack固有の環境変数です。localstackを調べると理解できると思います
    • HOSTNAME_EXTERNAL はcircleciのprimaryコンテナからlocalstackにアクセスできるようにcircleciのデフォルトhost名であるlocalhostに合わせます。これでterraformがlocalstackに付けて実行可能になります
    • AWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEYAWS_DEFAULT_REGION はaws cliを便利に使うために設定しました

steps説明

  • name: Wait for localstack
    • localstackコンテナが起動することを待ちます。一番前に置いた理由はterraform実行する前にlocalstackのもろもろ初期化作業する時間を稼ぐためです。当プロジェクトはlambdaとcloudwatch logs二つのサービスだけ使っていてこれで大丈夫ですが、localstackで使おうとするサービスが多い場合は大目にsleep 300などを入れることも考える必要があります

circleci local execute --job testによる作業の流れ

  1. circleci/python:3.7-buster-nodeコンテナ(Primaryコンテナ)が作られます
  2. localstackコンテナが作れます
  3. Primaryコンテナのstepが実行されます
  4. name: Wait for localstack stepで少なくともportが開けるまで待ちます
  5. pythonのpackageをディレクトリ/home/circleci/project/sam-lambda/hello_world/packages に指定し、インストールします
  6. $LAMBDA_ZIP_PATHに置きます
  7. terraformをインストールしあます
  8. terraformファイルがいる $TERRAFORM_PATH に移動します
  9. terraform initします
  10. terraform applyします。同時にterraformでlambdaのzipパス($LAMBDA_ZIP_PATH)とcircleciのデフォルトhost名を入れたを$COMMON_NETWORKを渡します
  11. terraformによってlambdaが作られます

circleci local execute --job testを叩いた結果

circleciの最後のstepに書いたlambdaをinvokeする部分がちゃんと動いてresponseのstatusが200を返したことが確認できます