Kinstaが本番サイクルのすべてのステップをドッカライズすることにより、エンドツーエンドの開発エクスペリエンスをどのように改善したか

ゲスト著者のアミン・チョルーミは、Kinstaの経験豊富なソフトウェア開発者です。 Docker と Kubernetes に情熱を注いでおり、アプリケーション開発と DevOps プラクティスを専門としています。 彼の専門知識は、これらの革新的なテクノロジーを活用して、展開プロセスを合理化し、ソフトウェアのスケーラビリティを強化することにあります。

エンタープライズ レベルでクラウドネイティブ アプリケーションを開発および保守する際の最大の課題の 1 つは、開発ライフサイクル全体を通じて一貫したエクスペリエンスを持つことです。 このプロセスは、分散したチームがさまざまなプラットフォーム、さまざまなセットアップ、非同期通信で作業するリモート企業にとってはさらに困難です。 

Kinstaには、アプリケーションホスティング、データベースホスティング、マネージドWordPressホスティングに関するあらゆる規模のプロジェクトがあります。一貫性があり、信頼性が高く、スケーラブルなソリューションを提供する必要があります。

  • 開発者と品質保証チームは、オペレーティング システムに関係なく、機能を開発およびテストするための簡単で最小限のセットアップを作成します。
  • DevOps、SysOps、およびインフラストラクチャ チームは、ステージング環境と運用環境を構成および保守します。
青の背景にキンスタのロゴと白い矢印

分散したチームでクラウドネイティブアプリケーションを開発するという課題を克服

Kinstaでは、開発から本番環境までのすべてのステップでこの一貫したエクスペリエンスを実現するために、Dockerに大きく依存しています。この記事では、以下について説明します。

  • Docker デスクトップ を活用して開発者の生産性を向上させる方法。
  • CircleCI と GitHub Actions を使用して Docker イメージをビルドし、CI パイプライン経由で Google Container Registry にプッシュする方法。
  • CD パイプラインを使用して、Docker イメージ、Google Kubernetes Engine、Cloud Deploy を使用して本番環境への増分変更を促進する方法。
  • QA チームがさまざまな環境で事前構築済みの Docker イメージをシームレスに使用する方法。

Docker デスクトップを使用して開発者エクスペリエンスを向上させる

アプリケーションをローカルで実行するには、開発者は環境を綿密に準備し、すべての依存関係をインストールし、サーバーとサービスをセットアップし、それらが適切に構成されていることを確認する必要があります。 複数のアプリケーションを実行する場合、このアプローチは、特に複数の依存関係を持つ複雑なプロジェクトの場合、面倒な場合があります。 また、複数のオペレーティング システムを持つ複数のコントリビューターを紹介すると、カオス がインストールされます。 これを防ぐために、Dockerを使用します。

Docker を使用すると、環境構成を宣言し、依存関係をインストールし、必要な場所にあるすべてのものを使用してイメージをビルドできます。 誰でも、どこでも、どのOSでも、同じイメージを使用し、他の人とまったく同じエクスペリエンスを得ることができます。

Docker Compose を使用して構成を宣言する

開始するには、 Docker 作成 ファイル docker-compose.yml(. これは、アプリケーションの望ましい状態を Docker に通知する YAML 形式で記述された宣言型構成ファイルです。 Docker はこの情報を使用して、アプリケーションの環境を設定します。

Docker Composeファイルは、複数のコンテナーを実行していて、コンテナー間に依存関係がある場合に便利です。

ファイルを作成するには docker-compose.yml :

  1. アプリケーションのベースとして を選択する image ことから始めます。 Docker Hub で検索して、アプリの依存関係が既に含まれている Docker イメージを見つけます。 エラーを避けるために、必ず特定のイメージタグを使用してください。 latest タグを使用すると、アプリケーションで予期しないエラーが発生する可能性があります。複数の依存関係に対して複数のベースイメージを使用できます (たとえば、PostgreSQL 用に 1 つ、Redis 用に 1 つ)。
  2. 必要に応じて、ホストにデータを保持するために使用します volumes 。 ホスト マシンにデータを保持すると、Docker コンテナーが削除された場合や、コンテナーを再作成する必要がある場合にデータが失われるのを防ぐことができます。
  3. ホストや他のコンテナーとのネットワークの競合を回避するために、セットアップを分離するために使用します networks 。 また、コンテナーが相互に簡単に検索して通信するのにも役立ちます。

すべてをまとめると、 docker-compose.yml 次のようになります。

version: '3.8'

services:
  db:
    image: postgres:14.7-alpine3.17
    hostname: mk_db
    restart: on-failure
    ports:
      - ${DB_PORT:-5432}:5432
    volumes:
      - db_data:/var/lib/postgresql/data
    environment:
      POSTGRES_USER: ${DB_USER:-user}
      POSTGRES_PASSWORD: ${DB_PASSWORD:-password}
      POSTGRES_DB: ${DB_NAME:-main}
    networks:
      - mk_network
  redis:
    image: redis:6.2.11-alpine3.17
    hostname: mk_redis
    restart: on-failure
    ports:
      - ${REDIS_PORT:-6379}:6379
    networks:
      - mk_network
      
volumes:
  db_data:

networks:
  mk_network:
    name: mk_network

アプリケーションをコンテナー化する

アプリケーションの Docker イメージをビルドする

まず、を使用して Docker イメージ Dockerfileを構築し、それを から docker-compose.yml呼び出す必要があります。

次の 5 つの手順に従ってファイルを作成します Dockerfile

1.ベースとして画像を選択することから始めます。 アプリで動作する最小の基本画像を使用します。 通常、高山のイメージは最小限で、追加のパッケージはほぼゼロです。 高山の画像から始めて、その上に構築することができます。

   docker
   FROM node:18.15.0-alpine3.17

2.競合を回避するために、特定のCPUアーキテクチャを使用する必要がある場合があります。 たとえば、プロセッサを使用している arm64-based が、 amd64 イメージを構築する必要があるとします。 これを行うには、 -- platform in を指定します Dockerfile

   docker
   FROM --platform=amd64 node:18.15.0-alpine3.17

3. アプリケーションディレクトリを定義し、依存関係をインストールして、出力をルートディレクトリにコピーします。

    docker
    WORKDIR /opt/app 
    COPY package.json yarn.lock ./ 
    RUN yarn install 
    COPY . .

4.から電話をかけ Dockerfile ます docker-compose.yml:

     services:
      ...redis
      ...db
      
      app:
        build:
          context: .
          dockerfile: Dockerfile
        platforms:
          - "linux/amd64"
        command: yarn dev
        restart: on-failure
        ports:
          - ${PORT:-4000}:${PORT:-4000}
        networks:
          - mk_network
        depends_on:
          - redis
          - db

5. 自動リロードを実装して、ソースコード内の何かを変更したときに、アプリケーションを手動で再構築しなくても、変更をすぐにプレビューできるようにします。 これを行うには、最初にイメージをビルドしてから、別のサービスで実行します。

     services:
      ... redis
      ... db
      
      build-docker:
        image: myapp
        build:
          context: .
          dockerfile: Dockerfile
      app:
        image: myapp
        platforms:
          - "linux/amd64"
        command: yarn dev
        restart: on-failure
        ports:
          - ${PORT:-4000}:${PORT:-4000}
        volumes:
          - .:/opt/app
          - node_modules:/opt/app/node_modules
        networks:
          - mk_network
        depends_on:
          - redis
          - db
          - build-docker

プロのヒント: node_modules パッケージに関するプラットフォーム固有の問題を回避するために、明示的にマウントされていることに注意してください。つまり、ホスト上で を使用する node_modules 代わりに、Docker コンテナーは独自のコンテナーを使用しますが、別のボリュームのホストにマップします。

継続的インテグレーションによる本番イメージの段階的な構築 

アプリとサービスの大部分はデプロイに CI/CD を使用しており、Docker はそのプロセスで重要な役割を果たします。 メインブランチで変更が行われるたびに、GitHub Actions または CircleCI を介してビルドパイプラインが即座にトリガーされます。 一般的なワークフローは単純で、依存関係のインストール、テストの実行、Docker イメージのビルド、Google コンテナ レジストリ(またはアーティファクト レジストリ)へのプッシュを行います。 この記事では、ビルド手順について説明します。

ドッカーイメージの構築

セキュリティとパフォーマンスの理由から、マルチステージビルドを使用しています。

ステージ1:ビルダー

この段階では、すべてのソースと構成を含むコード ベース全体をコピーし、開発依存関係を含むすべての依存関係をインストールして、アプリをビルドします。 フォルダーを作成し、 dist/ ビルドされたバージョンのコードをそこにコピーします。 ただし、この画像は大きすぎるため、運用に使用するには膨大なフットプリントのセットがあります。 また、プライベートNPMレジストリを使用するため、この段階でもプライベート NPM_TOKEN を使用します。 ですから、このステージを外の世界に出したくないのです。 この段階で必要なのはフォルダ dist/ だけです。

ステージ 2: 生産

ほとんどの人は、アプリを実行するために必要なステージに近いため、ランタイムにこのステージを使用します。 ただし、本番環境の依存関係をインストールする必要があるため、フットプリントを残して NPM_TOKEN. したがって、この段階はまだ公開する準備ができていません。 ここでは、19行目にも注意 yarn cache clean する必要があります。 この小さなコマンドは、画像サイズを最大60%削減します。

ステージ 3: ランタイム

最後のステージは、最小限のフットプリントで可能な限りスリムである必要があります。 したがって、完全にベイクされたアプリを本番環境からコピーして先に進みます。 私たちはそれらすべての糸と NPM_TOKEN ものを後ろに置き、アプリを実行するだけです。

これが最後 Dockerfile.productionです:

docker
# Stage 1: build the source code 
FROM node:18.15.0-alpine3.17 as builder 
WORKDIR /opt/app 
COPY package.json yarn.lock ./ 
RUN yarn install 
COPY . . 
RUN yarn build 

# Stage 2: copy the built version and build the production dependencies FROM node:18.15.0-alpine3.17 as production 
WORKDIR /opt/app 
COPY package.json yarn.lock ./ 
RUN yarn install --production && yarn cache clean 
COPY --from=builder /opt/app/dist/ ./dist/ 

# Stage 3: copy the production ready app to runtime 
FROM node:18.15.0-alpine3.17 as runtime 
WORKDIR /opt/app 
COPY --from=production /opt/app/ . 
CMD ["yarn", "start"]

すべての段階で、最初にファイルのコピー package.json yarn.lock を開始し、依存関係をインストールしてから、残りのコードベースをコピーすることに注意してください。 これは、Dockerが各コマンドを前のコマンドの上にレイヤーとして構築し、各ビルドが使用可能な場合は前のレイヤーを使用し、パフォーマンス目的でのみ新しいレイヤーをビルドできるためです。 

パッケージに触れずに何かを src/services/service1.ts 変更したとしましょう。 つまり、ビルダーステージの最初の4つのレイヤーは変更されておらず、再利用できます。 このアプローチにより、ビルドプロセスが非常に高速になります。

CircleCI パイプラインを介してアプリを Google コンテナ レジストリにプッシュする

CircleCI パイプラインで Docker イメージをビルドするには、いくつかの方法があります。 私たちの場合、以下を使用すること circleci/gcp-gcr orbsを選択しました:

Dockerのおかげで、アプリをビルドしてプッシュするには最小限の構成が必要です。

executors:
  docker-executor:
    docker:
      - image: cimg/base:2023.03
orbs:
  gcp-gcr: circleci/[email protected]
jobs:
  ...
  deploy:
    description: Build & push image to Google Artifact Registry
    executor: docker-executor
    steps:
      ...
      - gcp-gcr/build-image:
          image: my-app
          dockerfile: Dockerfile.production
          tag: ${CIRCLE_SHA1:0:7},latest
      - gcp-gcr/push-image:
          image: my-app
          tag: ${CIRCLE_SHA1:0:7},latest

GitHub アクションを使用してアプリを Google コンテナ レジストリにプッシュする

CircleCI の代わりに、GitHub Actions を使用してアプリケーションを継続的にデプロイすることもできます。

Dockerイメージをセットアップ gcloud してビルドし、次の場所にプッシュします gcr.io

jobs:
  setup-build:
    name: Setup, Build
    runs-on: ubuntu-latest

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

    - name: Get Image Tag
      run: |
        echo "TAG=$(git rev-parse --short HEAD)" >> $GITHUB_ENV

    - uses: google-github-actions/setup-gcloud@master
      with:
        service_account_key: ${{ secrets.GCP_SA_KEY }}
        project_id: ${{ secrets.GCP_PROJECT_ID }}

    - run: |-
        gcloud --quiet auth configure-docker

    - name: Build
      run: |-
        docker build \
          --tag "gcr.io/${{ secrets.GCP_PROJECT_ID }}/my-app:$TAG" \
          --tag "gcr.io/${{ secrets.GCP_PROJECT_ID }}/my-app:latest" \
          .

    - name: Push
      run: |-
        docker push "gcr.io/${{ secrets.GCP_PROJECT_ID }}/my-app:$TAG"
        docker push "gcr.io/${{ secrets.GCP_PROJECT_ID }}/my-app:latest"

小さな変更がメイン ブランチにプッシュされるたびに、新しい Docker イメージをビルドしてレジストリにプッシュします。

Google Delivery Pipelines を使用した Google Kubernetes Engine への変更のデプロイ

また、変更ごとにすぐに使用できるDockerイメージを用意することで、本番環境へのデプロイや、問題が発生した場合のロールバックも簡単になります。 Google では、アプリの管理と提供には Google Kubernetes Engine を使用し、継続的デプロイ プロセスには Google Cloud Deploy and Delivery Pipelines を使用しています。

小さな変更のたびに (前述の CI パイプラインを使用して) Docker イメージがビルドされたら、さらに一歩進んで、を使用して gcloud開発クラスターに変更をデプロイします。 CircleCI パイプラインのそのステップを見てみましょう。

- run:
    name: Create new release
    command: gcloud deploy releases create release-${CIRCLE_SHA1:0:7} --delivery-pipeline my-del-pipeline --region $REGION --annotations commitId=$CIRCLE_SHA1 --images my-app=gcr.io/${PROJECT_ID}/my-app:${CIRCLE_SHA1:0:7}

この手順により、開発用 Kubernetes クラスターの変更をロールアウトするためのリリース プロセスがトリガーされます。 テストして承認を取得した後、変更をステージングに昇格し、次に運用に昇格します。 このアクションはすべて、必要なほとんどすべてを備えた変更ごとにスリムな分離されたDockerイメージがあるためです。 使用するタグをデプロイに指示するだけで済みます。

品質保証チームがこのプロセスからどのように利益を得るか

QA チームは、主に実稼働前のクラウド バージョンのアプリをテストする必要があります。 ただし、特定の機能をテストするために、ビルド済みのアプリをローカルで(すべての依存関係とともに)実行する必要がある場合があります。 このような場合、プロジェクト全体のクローン作成、npmパッケージのインストール、アプリのビルド、開発者エラーへの直面、開発プロセス全体の作業など、アプリを稼働させるために苦労する必要はなく、必要もありません。

すべてが Google Container Registry で Docker イメージとして既に利用可能になったので、QA チームに必要なのは Docker 作成ファイルのサービスだけです。

services:
  ...redis
  ...db
  
  app:
    image: gcr.io/${PROJECT_ID}/my-app:latest
    restart: on-failure
    ports:
      - ${PORT:-4000}:${PORT:-4000}
    environment:
      - NODE_ENV=production
      - REDIS_URL=redis://redis:6379
      - DATABASE_URL=postgresql://${DB_USER:-user}:${DB_PASSWORD:-password}@db:5432/main
    networks:
      - mk_network
    depends_on:
      - redis
      - db

このサービスを使用すると、チームは次のコマンドを実行して、Docker コンテナーを使用してローカル コンピューター上のアプリケーションをスピンアップできます。

docker compose up

これは、テストプロセスを簡素化するための大きな一歩です。 QAがアプリの特定のタグをテストすることを決定した場合でも、6行目の画像タグを簡単に変更して、Docker作成コマンドを再実行できます。 アプリの異なるバージョンを同時に比較することにした場合でも、いくつかの調整で簡単に比較できます。 最大のメリットは、QAチームを開発者の課題から遠ざけることです。

ドッカーを使用する利点

  • 依存関係のフットプリントはほぼゼロです。 Redis または PostgreSQL のバージョンをアップグレードする場合は、1 行変更するだけでアプリを再実行できます。 システム上で何も変更する必要はありません。 さらに、両方がRedisを必要とする2つのアプリがある場合(バージョンが異なる場合でも)、互いに競合することなく、両方を独自の分離環境で実行できます。
  • アプリの複数のインスタンス: DBの初期化、テストの実行、DBの変更の監視、メッセージのリッスンなど、同じアプリを別のコマンドで実行する必要がある場合が多くあります。 いずれの場合も、ビルドされたイメージが既に準備ができているため、別のコマンドを使用して Docker 作成ファイルに別のサービスを追加するだけで完了です。
  • より簡単なテスト環境: 多くの場合、アプリを実行するだけです。 コード、パッケージ、またはローカル データベース接続は必要ありません。 アプリが正しく動作することを確認するか、独自のプロジェクトで作業している間にバックエンドサービスとして実行中のインスタンスが必要なだけです。 これは、QA、プルリクエストのレビュー担当者、またはデザインが適切に実装されていることを確認したいUX担当者にも当てはまります。 Dockerのセットアップにより、多くの技術的な問題に対処することなく、すべての人が簡単に作業を進めることができます。

さらに詳しく

フィードバック

「Kinstaが本番サイクルのすべてのステップをドッカライズすることにより、エンドツーエンドの開発エクスペリエンスをどのように改善したか」に関する0つの考え