継続的デリバリーにより、開発者、チーム、および組織は簡単にコードを更新し、顧客に新しい機能をリリースできます。チームや組織が CI/CD および DevOps プラクティスを採用すると、文化が変革され、これらのメリットを実現できます。CI/CD および DevOps のプラクティスを実装することにより、チームは、最新のツールを活用し、信頼性と一貫性のある方法で、ソフトウェアをビルド、テスト、およびデプロイできます。

Infrastructure as code(IaC)を使用すると、チームは、コード内でクラウドリソースを静的に定義および宣言し、コードからこれらのクラウドリソースをデプロイして、動的に維持でき、これらのリソースを簡単に管理できます。このブログでは、CI/CD パイプライン内で IaC を実装する方法について説明します。このブログでは、CircleCI のパートナーである Pulumi の Orb をパイプラインに実装する方法も説明します。この Orb は、アプリケーションを定義し、Google Kubernetes Engine(GKE)クラスタにデプロイします。Pulumi は、このブログで説明する IaC コンポーネントを提供します。

前提条件

このブログは、Git リポジトリがすでにあり、GitHub でホスティングされていることを前提としています。ここで提供しているコードサンプルは、プロジェクトのリポジトリ内のディレクトリに配置する必要があります。

使用するテクノロジー

このブログでは、以下について基本的な理解をしていることも前提としています。

Pulumi のセットアップ

Pulumi を使用すると、開発者は自分の好きな言語(JavaScript、Python、Go など)でコードを記述し、専用の DSL や YAML テンプレートソリューションを習得しなくても、クラウドアプリとインフラストラクチャを簡単にデプロイできます。ファーストクラス言語を使用すると、抽象化と再利用が可能になります。Pulumi は、すべての主要なクラウドサービスプロバイダー(AWS、Azure、GCP など)に対応するローレベルのリソース定義に加えて、ハイレベルのクラウドパッケージを提供しており、1 つのシステムをマスターするだけで、これらのすべてのプロバイダーにデリバリーできるようになります。

選択したアプリケーションリポジトリで、Pulumi アプリを配置する新しいディレクトリを作成します。

mkdir -p pulumi/gke
cd pulumi/gke

次に、Pulumi アカウントと Google Cloud アカウントにサインアップします(サインアップをしたことがない場合)。

新しい Pulumi プロジェクトを作成すると、pulumi/gke ディレクトリに次の 3 つのファイルが作成されます。

  • Pulumi.yaml - プロジェクトに関するメタデータを指定します。
  • Pulumi.<スタック名>.yaml - 初期化したスタックの構成値が含まれます。 <stack name> は、新しい Pulumi プロジェクトを作成するときに定義したスタック名に置き換える必要があります。このチュートリアルでは、このファイルに Pulumi.k8s.yaml という名前を付けています。
  • __main__.py - スタックリソースを定義する Pulumi プログラム。これは、IaC magicを実現する場所です。

Pulumi.<スタック名>.yaml ファイルを編集し、次のコードを貼り付けます。

config:
  gcp:credentials: ./cicd_demo_gcp_creds.json
  gcp:project: cicd-workshops
  gcp:region: us-east1
  gcp:zone: us-east1-d
  gke:name: k8s

__main__.py ファイルを編集し、その内容を次の内容に置き換えます。

import os
import pulumi
import pulumi_kubernetes
from pulumi import ResourceOptions
from pulumi_kubernetes.apps.v1 import Deployment
from pulumi_kubernetes.core.v1 import Namespace, Pod, Service
from pulumi_gcp import container

conf = pulumi.Config('gke')
gcp_conf = pulumi.Config('gcp')

stack_name = conf.require('name')
gcp_project = gcp_conf.require('project')
gcp_zone = gcp_conf.require('zone')

app_name = 'cicd-app'
app_label = {'appClass':app_name}
cluster_name = app_name

image_tag = ''
if 'CIRCLE_SHA1' in os.environ:
    image_tag = os.environ['CIRCLE_SHA1']
else:
    image_tag = 'latest'

docker_image = 'ariv3ra/orb-pulumi-gcp:{0}'.format(image_tag)

machine_type = 'g1-small'

cluster = container.Cluster(
    cluster_name,
    initial_node_count=3,
    min_master_version='latest',
    node_version='latest',
    node_config={
        'machine_type': machine_type,
        'oauth_scopes': [
            "https://www.googleapis.com/auth/compute",
            "https://www.googleapis.com/auth/devstorage.read_only",
            "https://www.googleapis.com/auth/logging.write",
            "https://www.googleapis.com/auth/monitoring",
        ],
    }
)

# Set the Kubeconfig file values here
def generate_k8_config(master_auth, endpoint, context):
    config = '''apiVersion: v1
clusters:
- cluster:
    certificate-authority-data: {masterAuth}
    server: https://{endpoint}
  name: {context}
contexts:
- context:
    cluster: {context}
    user: {context}
  name: {context}
current-context: {context}
kind: Config
preferences: {prefs}
users:
- name: {context}
  user:
    auth-provider:
      config:
        cmd-args: config config-helper --format=json
        cmd-path: gcloud
        expiry-key: '{expToken}'
        token-key: '{tokenKey}'
      name: gcp
    '''.format(masterAuth=master_auth, context=context, endpoint=endpoint, 
            prefs='{}', expToken = '{.credential.token_expiry}', tokenKey='{.credential.access_token}')

    return config

gke_masterAuth = cluster.master_auth['clusterCaCertificate']
gke_endpoint = cluster.endpoint
gke_context = gcp_project+'_'+gcp_zone+'_'+cluster_name

k8s_config = pulumi.Output.all(gke_masterAuth,gke_endpoint,gke_context).apply(lambda args: generate_k8_config(*args))

cluster_provider = pulumi_kubernetes.Provider(cluster_name, kubeconfig=k8s_config)
ns = Namespace(cluster_name, __opts__=ResourceOptions(provider=cluster_provider))

gke_deployment = Deployment(
    app_name,
    metadata={
        'namespace': ns,
        'labels': app_label,
    },
    spec={
        'replicas': 3,
        'selector':{'matchLabels': app_label},
        'template':{
            'metadata':{'labels': app_label},
            'spec':{
                'containers':[
                    {
                        'name': app_name,
                        'image': docker_image,
                        'ports':[{'name': 'port-5000', 'container_port': 5000}]
                    }
                ]
            }
        }
    },
    __opts__=ResourceOptions(provider=cluster_provider)
)

deploy_name = gke_deployment

gke_service = Service(
    app_name,
    metadata={
        'namespace': ns,
        'labels': app_label,
    },
    spec={
        'type': "LoadBalancer",
        'ports': [{'port': 80, 'target_port': 5000}],
        'selector': app_label,
    },
    __opts__=ResourceOptions(provider=cluster_provider)
)

pulumi.export("kubeconfig", k8s_config)
pulumi.export("app_endpoint_ip", gke_service.status['load_balancer']['ingress'][0]['ip'])

__main__.py ファイルのコンテンツは、パイプラインからデプロイする GKE クラスタとインフラストラクチャを指定します。この Pulumi アプリケーションは、Docker コンテナを介してポッドでアプリケーションを実行する 3 つのノードの Kubernetes クラスタを作成します。このコードは、さまざまなコンピュートノードのアクティブな Docker コンテナにトラフィックを均等にルーティングするロードバランサーリソースも作成します。Pulumi Python アプリの詳細については、このサイトをご覧ください。

Google Cloud のセットアップ

このセクションでは、必要となる GCP 認証情報を作成します。これらの認証情報により、CI/CD パイプラインと Pulumi コードに、GCP でコマンドを実行する権限が付与されます。

GCP プロジェクトの作成

新しいアカウントに対して、デフォルトのプロジェクトが設定されます。後で簡単にティアダウンできるように、新しいプロジェクトを作成して個別に管理しておくことをお勧めします。プロジェクトを作成した後は、プロジェクト ID を必ずコピーしておきます(この ID は、プロジェクト名とは異なります)。

How to find your project id.

プロジェクト認証情報の取得

次に、Pulumi が GCP プロジェクトのリソースを作成および管理するために使用するサービスアカウントキーを設定します。[サービスアカウントキーの作成] ページに移動します。デフォルトのサービスアカウントを選択するか、新しいサービスアカウントを作成し、[キーのタイプ] として [JSON] を選択して、[作成] をクリックします。この .json ファイルを pulumi/gke フォルダに保存します。

セキュリティ上の重要な注意: ファイル名を cicd_demo_gcp_creds.json に変更し、Google Cloud の認証情報が GitHub パブリックリポジトリに公開されないようにします。さらに、このプロジェクトの .gitignore ファイルに認証情報の .json ファイル名を追加できます。このファイルのデータには非常に慎重に扱ってください。このファイルデータが公開されると、第三者がこの情報を使用してアカウントにログインしてリソースを作成することが可能となり、Google Cloud アカウントの請求額が増大する恐れがあります。

CircleCI のセットアップ

次に、Pulumi を CI/CD パイプラインに統合するために CircleCI とパイプラインコンフィグファイルを構成する必要があります。

Google サービスアカウントファイルのエンコード

サービスアカウントファイルを base64 でエンコードしたデータを CircleCI の環境変数として保存する必要があります。ターミナルで次のコマンドを実行して値をエンコードし、結果を取得します。

base64 cicd_demo_gcp_creds.json

このコマンドの結果は次のようになります。

ewogICJ0eXBlIjogInNlcnZpY2VfYWNjb3VudCIsCiAgInByb2plY3RfaWQiOiAiY2ljZC13b3Jrc2hvcHMiLAogICJwcml2YXRlX2tleV9pZCI6ICJiYTFmZDAwOThkNTE1ZTE0NzE3ZjE4NTVlOTY1NmViMTUwNDM4YTQ4IiwKICAicHJpdmF0ZV9rZXkiOiAiLS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tXG5NSUlFdlFJQkFEQU5CZ2txaGtpRzl3MEJBUUVGQUFTQ0JLY3dnZ1NqQWdFQUFvSUJBUURjT1JuRXFla3F4WUlTXG5UcHFlYkxUbWdWT3VzQkY5NTE1YkhmYWNCVlcyZ2lYWjNQeFFBMFlhK2RrYjdOTFRXV1ZoRDZzcFFwWDBxY2l6XG5GdjFZekRJbXkxMCtHYnlUNWFNV2RjTWw3ZlI2TmhZay9FeXEwNlc3U0FhV0ZnSlJkZjU4U0xWcC8yS1pBbjZ6XG5BTVdHZjM5RWxSNlhDaENmZUNNWXorQmlZd29ya3Nob3BzLmlhbS5nc2VydmljZWFjY291bnQuY29tIgp9Cg==

次のセクションで使用するため、結果をクリップボードにコピーします。

プロジェクト変数の作成

この CI/CD パイプラインが GCP でコマンドを実行するためには、CircleCI でプロジェクトレベルの環境変数を構成する必要があります。

CircleCI ダッシュボードを使用して、次のプロジェクトレベルの環境変数を作成します。

  • $DOCKER_LOGIN = Docker Hub のユーザー名
  • $DOCKER_PWD = Docker Hubのパスワード
  • $GOOGLE_CLOUD_KEYS = 前のセクションで base64 でエンコードした結果
  • $PULUMI_ACCESS_TOKEN = Pulumi ダッシュボードからアクセストークンを生成します。

Pulumi を統合した CI/CD パイプライン

これで、Pulumi アプリを CircleCI の config.yml ファイルに統合するために必要なすべての要素を準備できました。config.yml を編集し、次のコンフィグをファイルに貼り付けます。この config.yaml の内容は、デモとプレゼンテーションで使用するサンプルの Python プロジェクトに固有であるため、お使いのプロジェクトの config.yml とは異なります。詳細なサンプルプロジェクトについては、こちらの GitHub レポジトリを参照してください。どのような処理が行われているのかが明確になるように、このコンフィグサンプルの Pulumi 統合の重要部分について、詳しく説明します。

version: 2.1
orbs:
  pulumi: pulumi/pulumi@1.0.1
jobs:
  build_test:
    docker:
      - image: circleci/python:3.7.2
        environment:
          PIPENV_VENV_IN_PROJECT: 'true'
    steps:
      - checkout
      - run:
          name: Install Python Dependencies
          command: |
            pipenv install --skip-lock
      - run:
          name: Run Tests
          command: |
            pipenv run pytest
  build_push_image:
    docker:
      - image: circleci/python:3.7.2
    steps:
      - checkout
      - setup_remote_docker:
          docker_layer_caching: false
      - run:
          name: Build and push Docker image
          command: |       
            pipenv install --skip-lock
            pipenv run pyinstaller -F hello_world.py
            echo 'export TAG=${CIRCLE_SHA1}' >> $BASH_ENV
            echo 'export IMAGE_NAME=orb-pulumi-gcp' >> $BASH_ENV
            source $BASH_ENV
            docker build -t $DOCKER_LOGIN/$IMAGE_NAME -t $DOCKER_LOGIN/$IMAGE_NAME:$TAG .
            echo $DOCKER_PWD | docker login -u $DOCKER_LOGIN --password-stdin
            docker push $DOCKER_LOGIN/$IMAGE_NAME
  deploy_to_gcp:
    docker:
      - image: circleci/python:3.7.2
        environment:
          CLOUDSDK_PYTHON: '/usr/bin/python2.7'
          GOOGLE_SDK_PATH: '~/google-cloud-sdk/'
    steps:
      - checkout
      - pulumi/login:
          access-token: ${PULUMI_ACCESS_TOKEN}
      - run:
          name: Install dependecies
          command: |
            cd ~/
            sudo pip install --upgrade pip==18.0 && pip install --user pulumi pulumi-gcp pulumi-kubernetes
            curl -o gcp-cli.tar.gz https://dl.google.com/dl/cloudsdk/channels/rapid/google-cloud-sdk.tar.gz
            tar -xzvf gcp-cli.tar.gz
            echo ${GOOGLE_CLOUD_KEYS} | base64 --decode --ignore-garbage > ${HOME}/project/pulumi/gcp/gke/cicd_demo_gcp_creds.json
            ./google-cloud-sdk/install.sh  --quiet 
            echo 'export PATH=$PATH:~/google-cloud-sdk/bin' >> $BASH_ENV
            source $BASH_ENV
            gcloud auth activate-service-account --key-file ${HOME}/project/pulumi/gcp/gke/cicd_demo_gcp_creds.json
      - pulumi/update:
          stack: k8s
          working_directory: ${HOME}/project/pulumi/gcp/gke/
workflows:
  build_test_deploy:
    jobs:
      - build_test
      - build_push_image:
          requires:
            - build_test
      - deploy_to_gcp:
          requires:
          - build_push_image

次のコードスニペットは、Pulumi Orb がこのパイプラインで使用されることを指定します。

version: 2.1
orbs:
  pulumi: pulumi/pulumi@1.0.1

サンプルパイプラインの jobs: キーには、3 つの個別のジョブが定義されています。

  • build_test: このジョブは、アプリケーションの単体テストを実行します。

  • build_push_image: このジョブは、通常、プロジェクトリポジトリに共存する Dockerfile に基づいて新しい Docker イメージをビルドします。

  • deploy_to_gcp: このジョブは、Pulumi Orb を介して GKE クラスタにデプロイします。

ここでは、build_push_image および deploy_to_gcp ジョブについて詳しく見ていきましょう。

build_push_image:

  build_push_image:
    docker:
      - image: circleci/python:3.7.2
    steps:
      - checkout
      - setup_remote_docker:
          docker_layer_caching: false
      - run:
          name: Build and push Docker image
          command: |       
            pipenv install --skip-lock
            pipenv run pyinstaller -F hello_world.py
            echo 'export TAG=${CIRCLE_SHA1}' >> $BASH_ENV
            echo 'export IMAGE_NAME=orb-pulumi-gcp' >> $BASH_ENV
            source $BASH_ENV
            docker build -t $DOCKER_LOGIN/$IMAGE_NAME -t $DOCKER_LOGIN/$IMAGE_NAME:$TAG .
            echo $DOCKER_PWD | docker login -u $DOCKER_LOGIN --password-stdin
            docker push $DOCKER_LOGIN/$IMAGE_NAME

アプリケーションがテストされ正しくパスすると、build_push_image ジョブにより、このアプリケーションが単一の実行可能バイナリにパッケージングされます。次に、プロジェクトのリポジトリにある Dockerfile に基づいて、新しい Docker イメージをビルドする docker build コマンドを実行します。このジョブは既存の環境変数も使用しますが、一意の Docker イメージ名を指定するために使用されるいくつかの新しい環境変数も定義します。このプロジェクトの Dockerfile を以下に示します。

FROM python:3.7.2
RUN mkdir /opt/hello_world/
WORKDIR /opt/hello_world/
COPY dist/hello_world /opt/hello_world/
EXPOSE 80
CMD [ "./hello_world" ]

docker push コマンドにより、新しくビルドされた Docker イメージが Docker Hub にアップロードおよび保管され、今後取得できるようになります。

deploy_to_gcp:

  deploy_to_gcp:
    docker:
      - image: circleci/python:3.7.2
        environment:
          CLOUDSDK_PYTHON: '/usr/bin/python2.7'
          GOOGLE_SDK_PATH: '~/google-cloud-sdk/'
    steps:
      - checkout
      - pulumi/login:
          access-token: ${PULUMI_ACCESS_TOKEN}
      - run:
          name: Install dependencies
          command: |
            cd ~/
            sudo pip install --upgrade pip==18.0 && pip install --user pulumi pulumi-gcp pulumi-kubernetes
            curl -o gcp-cli.tar.gz https://dl.google.com/dl/cloudsdk/channels/rapid/google-cloud-sdk.tar.gz
            tar -xzvf gcp-cli.tar.gz
            echo ${GOOGLE_CLOUD_KEYS} | base64 --decode --ignore-garbage > ${HOME}/project/pulumi/gcp/gke/cicd_demo_gcp_creds.json
            ./google-cloud-sdk/install.sh  --quiet 
            echo 'export PATH=$PATH:~/google-cloud-sdk/bin' >> $BASH_ENV
            source $BASH_ENV
            gcloud auth activate-service-account --key-file ${HOME}/project/pulumi/gcp/gke/cicd_demo_gcp_creds.json
      - pulumi/update:
          stack: k8s
          working_directory: ${HOME}/project/pulumi/gcp/gke/

上記で指定した deploy_to_gcp: ジョブは、Pulumi アプリと Orb を使用し、GCP で新しい GKE クラスタを実際に立ち上げるパイプラインの一部です。以下では、この deploy_to_gcp: ジョブについて簡単に説明します。

      - pulumi/login:
          access-token: ${PULUMI_ACCESS_TOKEN}

上記のコードは、Pulumi Orb の login: コマンドの仕様と実行を示しています。access-token: パラメーターには、CircleCI ダッシュボードで設定した ${PULUMI_ACCESS_TOKEN} 環境変数が渡されます。

            curl -o gcp-cli.tar.gz https://dl.google.com/dl/cloudsdk/channels/rapid/google-cloud-sdk.tar.gz
            tar -xzvf gcp-cli.tar.gz
            echo ${GOOGLE_CLOUD_KEYS} | base64 --decode --ignore-garbage > ${HOME}/project/pulumi/gcp/gke/cicd_demo_gcp_creds.json
            ./google-cloud-sdk/install.sh  --quiet 
            echo 'export PATH=$PATH:~/google-cloud-sdk/bin' >> $BASH_ENV
            source $BASH_ENV
            gcloud auth activate-service-account --key-file ${HOME}/project/pulumi/gcp/gke/cicd_demo_gcp_creds.json  

上記のコマンドは、Google Cloud SDK をダウンロードしてインストールします。この SDK は、GCP で GKE クラスタを作成および変更するために必要です。最初の 2 行は、SDK をダウンロードして解凍します。echo ${GOOGLE_CLOUD_KEYS} | base64 --decode... コマンドは、${GOOGLE_CLOUD_KEYS} 環境変数をデコードし、cicd_gcp_creds.json にデコードしたコンテンツを入力します。このファイルは、Pulumi アプリプロジェクトのディレクトリに存在する必要があります。この特定の run: ブロックのコマンドの残り部分は、SDK をインストールします。最後の行は、cicd_demo_gcp_creds.json ファイルから GCP にアクセスするサービスアカウントを許可します。

  - pulumi/update:
      stack: k8s
      working_directory: ${HOME}/project/pulumi/gcp/gke/

上記のコードは、Pulumi Orb の update: コマンドを使用して、GCP 上の新しい GKE クラスタへのアプリケーションのデプロイを開始します。pulumi/update: コマンドは、stack: および working_directory: パラメーターを表示します。これらのパラメーターは、Pulumi スタックの名前と Pulumi プロジェクトとして初期化されたディレクトリへのファイルパスをそれぞれ示します。お使いのプロジェクトでの working_directory: は、上記のコードサンプルとは異なります。

結論

このブログでは、Infrastructure as Code(IaC)ソリューションを CI/CD パイプラインに統合する方法について説明しました。また、CI/CD パイプラインで CircleCI Orb テクノロジーを宣言して実行する方法についても説明しました。これらのサンプルを見ると、CI/CD オートメーションによる IaC ソリューションを使用したコードのビルド、テスト、およびデプロイを深く理解できるようになります。

CircleCI Orbs を使用すると、CircleCI コンフィグの記述が簡素化されるため、生産性が向上します。Orbs は共有できるため、作成済みのコマンド、ジョブ、および Executor をコンフィグファイルで使用でき、時間を節約できます。Orbs の使用は CircleCI と GKE のデプロイに限定されません。Orb レジストリで利用可能な Orbs のリストを確認し、クラウドプラットフォーム、プログラミング言語など対応する Orbs を見つけることができます。

詳細なサンプルプロジェクトについては、こちらの GitHub レポジトリを参照してください。