ecspresso+ecschedule+lambrollでCI/CDを作った話

前回の記事から間が空いてしまいました、SREのbutadoraです。
年末に向けた準備で忙しなくしているこの頃です。

今回はとある環境で実装したCI/CDのフローを紹介したいと思います。

今回のサービスアーキテクチャ

今回はPHPWEBサービスをデプロイする環境が必要ということで、以下の様な設計としました。

  • WEBサービス本体 → ALB+ECS+RDS
  • 定時バッチサービス → ECS (Task Scheduler)+RDS
  • ファイル設置をトリガーにしたバッチサービス → S3+Lambda(コンテナイメージ)+RDS

CI/CD

簡単な構成図はこんな感じです。

大きなポイントとしては、タイトルにある3種の各デプロイツールを組み合わせることで、開発側のリソース管理を切り出しているところです。

弊社ではAWSリソースの管理をTerraformで行っていますが、図にあるようなリソースまで管理対象としてしまうと、 SRE側の運用に伴うデプロイサイクルと開発によるデプロイサイクルで衝突が発生してしまいます。

そこで、各サービスで利用する以下サービスのリソース管理をTerraformでは行わず、開発側のリポジトリでコード管理しています。

  • ECS TaskDefinition、Service Definition
  • ECS TaskScheduler
  • ECR(Docker Image)
  • Lambdaリソース

これにより、開発と運用のデプロイサイクルを分離することができました🎉

各デプロイツールのご紹介

ここで今回のデプロイ3種の神器をご紹介します。

ecspresso

github.com

  • fujiwaraさんによって作成された、ECS周りのデプロイツールです
    • 詳しくは公式解説本をどうぞ zenn.dev
  • 今回はECS TaskDefinitionとService Definitionの管理をしてもらっています

lambroll

github.com

  • こちらもfujiwaraさんによって作成された、Lambdaリソースに特化したのデプロイツールです
  • 今回はlambda functionの管理をしてもらっています
    • 通常のランタイムを使う管理であれば、デプロイ対象ディレクトリのzipアーカイブ〜S3アップロードもやってもらうところですが、今回はDockerコンテナイメージで開発されたスクリプトとなるため、Lambda functionの管理だけとなっています

ecschedule

github.com

  • こちらはSongmuさんによって作成されたECS Scheduled Taskに特化した管理ツールです
  • 詳しくはSongmuさんのブログをどうぞ songmu.jp
  • 今回は言うまでもなくECS Scheduled Taskを管理してもらっています

実際のソースコード

ディレクトリ構造

CI/CDを構成するディレクトリは以下のとおりです

  • /.github/workflows
    • Github ActionsのYAMLファイルが環境ごとに並んでいます(後述)
  • /dockerディレクト
    • 各サービスごとにDockerfileとその他設定ファイルなどが配置されています
  • /deployディレクト
    • deploy 配下は以下のようにAWSリソース単位でサブディレクトリを掘って、それぞれに紐づくYAMLJSONファイルを設置しています

Github Actions

デプロイ先環境ごとに以下のようなファイルを設置しています。

name: Build and Deploy

on:
  push:
    branches:
      - master
    paths-ignore:
      - xxx
      - yyy

env:
  ENV: <環境名>
  AWS_REGION: ap-northeast-1
  IMAGE_TAG: ${{ github.sha }}
  TFSTATE_BUCKET: <tfstate bucket名>

defaults:
  run:
    shell: bash

jobs:
  build:
    name: Build and Push Docker Image
    runs-on: ubuntu-latest
    environment:
      name: prd
    strategy:
      matrix:
        docker: ["aaa", "bbb", "ccc", "ddd"]

    steps:
      - name: Checkout
        uses: actions/checkout@v2

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: ${{ secrets.アクセスキー }}
          aws-secret-access-key: ${{ secrets.シークレットアクセスキー }}
          aws-region: ${{ env.AWS_REGION }}

      - name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v1

      - name: Build, tag, and push image to Amazon ECR
        uses: docker/build-push-action@v2
        env:
          DOCKER_BUILDKIT: 1
          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
          ECR_REPOSITORY: service-name-${{ matrix.docker }}
        with:
          context: .
          file: ./docker/${{ matrix.docker }}/Dockerfile
          push: true
          tags: ${{ format('{0}/{1}:{2}', env.ECR_REGISTRY, env.ECR_REPOSITORY, env.IMAGE_TAG) }}
          target: production
          build-args: |
            XXX=xxx
            YYY=yyy

  deploy-ecspresso:
    name: Deploy with ecspresso
    needs: build
    runs-on: ubuntu-latest
    environment:
      name: prd
      url: xxxx

    steps:
      - name: Checkout
        uses: actions/checkout@v2

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: ${{ secrets.アクセスキー }}
          aws-secret-access-key: ${{ secrets.シークレットアクセスキー }}
          aws-region: ${{ env.AWS_REGION }}

      - name: Checkout ecspresso
        uses: kayac/ecspresso@v1
        with:
          version: v1.7.13

      - name: Register task definition
        run: ecspresso register --config deploy/ecs/ecspresso.yaml

      - name: Migrate database
        run: |
          ecspresso run --config deploy/ecs/ecspresso.yaml --latest-task-definition \
            --overrides='{"containerOverrides":[
              {"name": "service-${{ env.ENV }}-app", "command": ["php", "hoge"]},

      - name: Deployment with ecspresso
        run: |
          ecspresso deploy --config deploy/ecs/ecspresso.yaml

  deploy-ecschedule:
    name: Deploy with ecschedule
    needs: build
    runs-on: ubuntu-latest
    environment:
      name: prd

    steps:
      - name: Checkout
        uses: actions/checkout@v2

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: ${{ secrets.アクセスキー }}
          aws-secret-access-key: ${{ secrets.シークレットアクセスキー }}
          aws-region: ${{ env.AWS_REGION }}

      - name: Checkout ecschedule
        uses: Songmu/ecschedule@main
        with:
          version: v0.4.0

      - name: Deployment with ecschedule
        env:
          PRIVATE_SUBNET_ID_AZA: "- subnet-XXXXX"
          PRIVATE_SUBNET_ID_AZC: "- subnet-YYYYYY"
          PRIVATE_SUBNET_ID_AZD: "- subnet-ZZZZZZ"
          SECURITY_GROUP_ID: sg-AAAAA
          ECS_EVENTS_ROLE: "arn:aws:iam::123456789012:role/<ruleに割り当てるrole名>"
        run: |
          ecschedule -conf deploy/ecs/ecschedule.yaml apply \
            -rule rule-name

  deploy-lambroll:
    name: Deploy with lambroll
    needs: build
    runs-on: ubuntu-latest
    environment:
      name: prd

    steps:
      - name: Checkout
        uses: actions/checkout@v2

      - name: Configure AWS credentials
        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: ${{ env.AWS_REGION }}

      - name: Checkout lambroll
        uses: fujiwara/lambroll@v0
        with:
          version: v0.12.7

      - name: Deployment with lambroll
        env:
          DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
          LOG_LEVEL: error
        run: |
          lambroll deploy --function=deploy/lambda/function.json \
            --tfstate="s3://${TFSTATE_BUCKET}/service.tfstate"

デプロイに必要な権限(IAM Policy)

ecspresso

  • こちらの記事を参考にさせていただきました!🙏 zenn.dev
  • 不要な権限を削った程度ですので、今回は省略します

lambroll

  • 過去に別のところで書いたlambroll最小権限から少し追加が発生したので、書いておきます
  • 今回の要件としては以下の通り
{
    "Statement": [
        {
            "Action": [
                "ec2:DescribeVpcs",
                "ec2:DescribeSubnets",
                "ec2:DescribeSecurityGroups"
            ],
            "Effect": "Allow",
            "Resource": "*",
            "Sid": ""
        },
        {
            "Action": [
                "lambda:UpdateFunctionConfiguration",
                "lambda:UpdateFunctionCode",
                "lambda:UpdateAlias",
                "lambda:ListTags",
                "lambda:GetFunction",
                "lambda:CreateFunction",
                "lambda:CreateAlias"
            ],
            "Effect": "Allow",
            "Resource": "arn:aws:lambda:ap-northeast-1:123456789012:function:<デプロイ対象function名>",
            "Sid": ""
        },
        {
            "Action": "iam:PassRole",
            "Effect": "Allow",
            "Resource": "arn:aws:iam::123456789012:role/<lambdaに割り当てるrole名>",
            "Sid": ""
        },
        {
            "Action": "s3:GetObject",
            "Effect": "Allow",
            "Resource": "arn:aws:s3:::<tfstate bucket名>/<terraform state file名>.tfstate",
            "Sid": ""
        }
    ],
    "Version": "2012-10-17"
}

ecschedule

  • ECS Scheduled Taskの実態はEventBridgeなので、主にそのあたりの権限
  • 具体的なPolicyとしては以下の通り。
{
    "Statement": [
        {
            "Action": [
                "events:ListRules",
                "ecs:DescribeTaskDefinition"
            ],
            "Effect": "Allow",
            "Resource": "*",
            "Sid": ""
        },
        {
            "Action": [
                "events:PutTargets",
                "events:PutRule",
                "events:ListTargetsByRule"
            ],
            "Effect": "Allow",
            "Resource": "arn:aws:events:ap-northeast-1:123456789012:rule/<デプロイ対象rule名>",
            "Sid": ""
        },
        {
            "Action": "iam:PassRole",
            "Effect": "Allow",
            "Resource": "arn:aws:iam::123456789012:role/<ruleに割り当てるrole名>",
            "Sid": ""
        }
    ],
    "Version": "2012-10-17"
}

あとがき

TerraformやCloudFormation等一元的なリソース管理事例が多い中、デプロイサイクルを意識したCI/CDフローの確立を各種サービスに特化したOSSを利用することで解決する良い例になったと感じています。

また、以前にgo runtimeなLambda functionを対象としてlambrollを使ったCI/CDフローを構築しましたが、その直後にDockerコンテナイメージがLambda/lambroll共にサポートされてからようやく試すことができました💪

今年の春頃に構築後、下書きに入れてる間に今回のサービスは世に出ないものとなってしまうようなので供養記事ということで🙏

次はアドベントカレンダーでお会いしましょう🎉

qiita.com

宣伝

香西がCloudNativeDays登壇します!

今回の記事でもとりあげた、ecspressoを活用したCI/CDフローをよりDeepに語ってくれる予定です!

event.cloudnativedays.jp

エンジニア募集

弊社ではSREを絶賛募集中です。 興味がある方はぜひ一度お話しましょう!

www.wantedly.com