はじめに
開発中のクラウドインフラとアプリのプロビジョニングツールであるDacraneを利用して、APIサーバをAzure App Serviceにデプロイする例をご紹介したいと思います。
Dacraneとは
サイオステクノロジーで開発しているクラウドインフラとアプリまでをデプロイするプロビジョニングツールです。現在の最新バージョンは0.0.7の開発初期段階のOSSです。IaCツール(特にTerraform)の影響を受けており、宣言的なコードでデプロイメント扱うことができます。
特徴としては、
- YAMLによる宣言的なデプロイ定義
- デプロイプロセスをコンテナ上で動かすことによる可搬性の高さ
- インフラからアプリまで一式がデプロイできる
という点があげられます。
今回はDacraneで簡単なAPIをデプロイする方法をご紹介したいと思います。
環境
- macOS Ventura
- Go 1.21
- Docker Desktop 4.25.0
- GNU Make 3.81
手順
Dacraneをインストールする
Dacraneのインストール手順を説明します。基本的な手順はリポジトリをクローンしてビルドを実行します。
現在のバージョンはbrew
やyum
、apt
などのパッケージ管理ツールでは配布していないので、ご自身の環境でビルドいただく必要があります。
まずはDacraneのリポジトリ(Dacrane 0.0.7)をクローンします。
$ git clone https://github.com/SIOS-Technology-Inc/dacrane.git -b 0.0.7
$ cd dacrane
次のコマンドで、/user/local/bin
にdacrane
コマンドをインストールします。
$ make install
次のコマンドでDacraneが利用するプラグインをインストールします。プラグインはコンテナです。現在はローカルでプラグインをビルドしていますが、今後はDockerHubなどからビルド済みイメージをプルして使えるようにすることを想定しています。
$ make install-plugin
次のように表示されればインストール完了です。
$ dacrane --help
Usage:
dacrane [command]
Available Commands:
apply create or update resource
completion Generate the autocompletion script for the specified shell
destroy destroy resource and artifact
help Help about any command
init initialize dacrane project
ls show instance list
Flags:
-h, --help help for dacrane
Use "dacrane [command] --help" for more information about a command.
作業ディレクトリを準備する
作業ディレクトリを作成します。
$ mkdir work
$ cd work/
次にサンプルコードディレクトリからアプリケーションのコードをコピーします。
$ cp ../example/app-services/index.js ./index.js
$ cp ../example/app-services/package-lock.json ./package-lock.json
$ cp ../example/app-services/package.json ./package.json
ディレクトリには次のようなファイルが格納されています。
$ ls
index.js package-lock.json package.json
index.jsはExpress.js製の簡単なAPIサーバのコードです。
const express = require('express')
const app = express()
// respond with "hello world" when a GET request is made to the homepage
app.get('/', (req, res) => {
res.send('hello world')
})
const port = process.env.PORT || 3000
app.listen(port, () => {
console.log(`Start server on ${port}!`);
})
このコードをデプロイしていきます。
ローカルのDcokerにデプロイする
まずはいきなりApp Serviceにデプロイするのではなく、ローカルのDocker上にデプロイしてみましょう。
Dacraneでデプロイメントを定義するにはdacrane.yaml
というファイルを作成し、「モジュール(Module)」と呼ばれるデプロイの基本単位を定義します。モジュールはYAMLの1ドキュメントとして定義します。
コンテナビルド
最初にコンテナビルドするモジュール定義をします。定義は次のようになります。
name: api-image # モジュール名
parameter: # モジュール引数
type: object
properties:
tag: { type: string, default: "latest" }
import: # 外部モジュールインポート
- https://raw.githubusercontent.com/SIOS-Technology-Inc/dacrane/0.0.7/module/docker-npm.yaml
modules: # 他モジュール、プラグイン呼び出し。
- name: api-local-image
module: docker-npm # npmでビルドされるdockerイメージを作るモジュールを呼び出す
argument:
image: sample-api
tag: ${{ parameter.tag }}
モジュールはプログラミング言語における関数のような概念で、名前(name
)、引数(parameter
)、本体(modules
)を持ちます。このモジュールapi-image
は引数tag
を受け取り、sample-api
という名前でDockerイメージをビルドするという定義です。
では、早速これを実行してみましょう。
実行前にプロジェクトの初期化が必要です。dacrane init
で行います。プロジェクトで1度のみ実行すればOKです。
$ dacrane init
初期化を実行すると.dacrane
フォルダが作成されます。この中に作成したインフラやアプリの状態が記録されます。
$ ls -a
. .dacrane index.js package.json
.. dacrane.yaml package-lock.json
次を実行するとコンテナイメージのビルドが走ります。このコマンドのapi-image
は実行したいモジュール名です。api-latest
はインスタンス名といって、このモジュールから作られるビルド成果物の実体に名前をつけています。-a
はモジュールに与える引数です。デフォルトが定められていれば省略することが可能です。
注:現在は引数はYAMLで渡すことになっていますが、YAMLをコマンドラインで渡すのは、辛いのでこの仕様は変更する可能性高いです。
$ dacrane apply api-image api-latest -a '{ tag: latest }'
[api-latest (docker-npm)] Evaluating...
[api-latest (docker-npm)] Evaluated.
[api-latest.api-local-image (local/resource/file)] Evaluating...
...
#10 writing image sha256:163888daf501aa2201fc0bc050d95dab7d6e2f60aa53aa137741d5bf51f0335f done
#10 naming to docker.io/library/sample-api:latest done
#10 DONE 0.2s
[api-latest.api-local-image.image (docker/resource/local-image)] Created.
これでイメージのビルドが完了しました。
dacrane ls
で作ったインスタンスの一覧を表示することができます。[instance_name] ([module_name])
を表しています。
$ dacrane ls
api-latest (api-image)
次にコンテナを起動の定義を実施します。api-image
の下に---
でドキュメントを区切って定義します。(記載の仕方がわからない場合は最後に完全版のコードを載せています。)
定義は次のとおりです。
コンテナ起動
name: api-image # モジュール名
// ...(省略)...
---
name: local-docker
parameter:
type: object
properties:
image: { type: string, default: sample-api }
tag: { type: string, default: latest }
modules:
- module: docker/resource/container
name: api
argument:
name: api
image: ${{ parameter.image }}
tag: ${{ parameter.tag }}
port: 3000:3000
env:
- name: PORT
value: "3000"
同様にコンテナを起動します。
$ dacrane apply local-docker api
[api (docker/resource/container)] Evaluating...
[api (docker/resource/container)] Evaluated.
[api.api (docker/resource/container)] Creating...
> docker run -d --name api -p 3000:3000 -e "PORT=3000" sample-api:latest
5a1648503aebb4f8edcad2dd1df9e2a990b069da25a19209ff6e853fa7be27ce
[api.api (docker/resource/container)] Created.
これでコンテナがデプロイできました。
APIに疎通確認しています。
$ curl http://localhost:3000
hello world
無事デプロイされました。
App Serviceにデプロイする
サービスプリンシパルの作成
次にAuzreのApp Serviceにデプロイしてみます。Azureのアカウントが必要になるのでお持ちでない方は事前にご準備ください。
アカウントができましたら、dacrane用のサービスプリンシパルを用意します。サービスプリンシパルの作成のコマンドは次のとおりです。(Azure CLIを使わない場合は、Azureポータル上でも作成できます。その際にContributor
ロールをつけて、クライアントシークレットを発行してください。)
$ az ad sp create-for-rbac --name "dacrane-${your_name}" --role="Contributor" --scopes="/subscriptions/${your_subscription_id}"
{
"appId": "00000000-0000-0000-0000-000000000000",
"displayName": "dacrane-your-name",
"password": "12345678-0000-0000-0000-000000000000",
"tenant": "10000000-0000-0000-0000-000000000000"
}
サービスプリンシパルが準備できたら、その情報を環境変数に格納してください。次のように.env
ファイルに環境変数を定義して、source .env
で読み込むと便利です。
export ARM_CLIENT_ID="00000000-0000-0000-0000-000000000000"
export ARM_CLIENT_SECRET="12345678-0000-0000-0000-000000000000"
export ARM_TENANT_ID="10000000-0000-0000-0000-000000000000"
export ARM_SUBSCRIPTION_ID="20000000-0000-0000-0000-000000000000"
$ source .env
これでDacraneでAzureのリソースを操作する準備が整いました。
基本リソースの作成
App Serviceを作成する前にResource GroupとAzure Container Registryを構築します。
定義は次のとおりです。
# (省略)他のリソース
---
name: base
parameter:
type: object
required: ["prefix"]
properties:
prefix: { type: string }
modules:
- name: rg
# terraformのazurerm_resource_groupリソースを呼んでいる。
module: terraform/resource/azurerm_resource_group
argument:
provider: &config
features: {}
client_id: ${{ env.ARM_CLIENT_ID }}
client_secret: ${{ env.ARM_CLIENT_SECRET }}
tenant_id: ${{ env.ARM_TENANT_ID }}
subscription_id: ${{ env.ARM_SUBSCRIPTION_ID }}
resource:
name: ${{ parameter.prefix }}-sample-rg
location: "Japan East"
- name: acr
# terraformのazurerm_container_registryリソースを呼んでいる。
module: terraform/resource/azurerm_container_registry
argument:
provider: *config
resource:
name: ${{ parameter.prefix }}sampleacr
resource_group_name: ${{ modules.rg.name }}
location: "Japan East"
sku: Basic
admin_enabled: true
この定義ではterraform
プラグインを呼び出し、リソースを定義しています。Terraformのプロセスはプラグインのコンテナ上で動くので個別にコマンドをインストールする必要はありません。
yourname
に任意の名称を入れて構築します。
$ dacrane apply base base -a '{ prefix: yourname }'
[base (terraform/resource/azurerm_resource_group)] Evaluating...
[base (terraform/resource/azurerm_resource_group)] Evaluated.
[base.rg (terraform/resource/azurerm_resource_group)] Creating...
Terraform apply complete
[base.rg (terraform/resource/azurerm_resource_group)] Created.
[base (terraform/resource/azurerm_container_registry)] Evaluating...
[base (terraform/resource/azurerm_container_registry)] Evaluated.
[base.acr (terraform/resource/azurerm_container_registry)] Creating...
Terraform apply complete
[base.acr (terraform/resource/azurerm_container_registry)] Created.
base
が完了しました。
$ dacrane ls
base (base)
api (local-docker)
api-latest (api-image)
ACRにコンテナをプッシュする
次にACRにコンテナイメージをプッシュします。モジュールの定義は次のようになります。
# (省略)他のリソース
---
name: api-remote-image
parameter:
type: object
properties:
tag: { type: string, default: "latest" }
image: { type: string, default: "sample-api" }
acr: { type: object }
modules:
- name: api-remote-image
module: docker/resource/remote-image
argument:
image: ${{ parameter.image }}
tag: ${{ parameter.tag }}
remote: # プッシュ先のコンテナリポジトリ
url: ${{ parameter.acr.login_server }}
user: ${{ parameter.acr.admin_username }}
password: ${{ parameter.acr.admin_password }}
これを実行するとACRにコンテナイメージがプッシュされます。このとき、先ほど作成したACRのインスタンスを引数として渡す必要があります。
$ dacrane apply api-remote-image remote-latest \
-a '{ acr: "${{ instances.base.modules.acr }}" }'
[remote-latest (docker/resource/remote-image)] Evaluating...
[remote-latest (docker/resource/remote-image)] Evaluated.
[remote-latest.api-remote-image (docker/resource/remote-image)] Creating...
...
latest: digest: sha256:d0e684f4b2a5a11031e6a93c331d24d7812e19c8526961fef17981e9720cf5ea size: 2837
[remote-latest.api-remote-image (docker/resource/remote-image)] Created.
App Serviceのデプロイ
では最後にこれをApp Service上にデプロイしましょう。定義は次のようになります。
# (省略)他のリソース
---
name: app-service
argument:
parameter:
type: object
required: ["base"]
properties:
base: { type: object }
tag: { type: string, default: "latest" }
image: { type: string, default: "sample-api" }
modules:
- name: asp
module: terraform/resource/azurerm_app_service_plan
argument:
provider:
features: {}
client_id: ${{ env.ARM_CLIENT_ID }}
client_secret: ${{ env.ARM_CLIENT_SECRET }}
tenant_id: ${{ env.ARM_TENANT_ID }}
subscription_id: ${{ env.ARM_SUBSCRIPTION_ID }}
resource:
name: ${{ parameter.base.parameter.prefix }}-sample-asp
resource_group_name: ${{ parameter.base.modules.rg.name }}
location: "Japan East"
kind: "Linux"
reserved: true
sku:
tier: Basic
size: B1
- name: as
module: terraform/resource/azurerm_app_service
argument:
provider:
features: {}
client_id: ${{ env.ARM_CLIENT_ID }}
client_secret: ${{ env.ARM_CLIENT_SECRET }}
tenant_id: ${{ env.ARM_TENANT_ID }}
subscription_id: ${{ env.ARM_SUBSCRIPTION_ID }}
resource:
name: ${{ parameter.base.parameter.prefix }}-sample-as
resource_group_name: ${{ parameter.base.modules.rg.name }}
location: "Japan East"
app_service_plan_id: ${{ modules.asp.id }}
site_config:
linux_fx_version: DOCKER|${{ parameter.base.modules.acr.login_server }}/${{ parameter.image }}:${{ parameter.tag }}
app_settings:
DOCKER_REGISTRY_SERVER_URL: ${{ parameter.base.modules.acr.login_server }}
DOCKER_REGISTRY_SERVER_USERNAME: ${{ parameter.base.modules.acr.admin_username }}
DOCKER_REGISTRY_SERVER_PASSWORD: ${{ parameter.base.modules.acr.admin_password }}
WEBSITES_PORT: "3000"
いよいよApp ServiceにAPIをデプロイします。
$ dacrane apply app-service as \
-a '{ base: "${{ instances.base }}" }'
[as (terraform/resource/azurerm_app_service_plan)] Evaluating...
[as (terraform/resource/azurerm_app_service_plan)] Evaluated.
[as.asp (terraform/resource/azurerm_app_service_plan)] Creating...
Terraform apply complete
[as.asp (terraform/resource/azurerm_app_service_plan)] Created.
[as (terraform/resource/azurerm_app_service)] Evaluating...
[as (terraform/resource/azurerm_app_service)] Evaluated.
[as.as (terraform/resource/azurerm_app_service)] Creating...
Terraform apply complete
[as.as (terraform/resource/azurerm_app_service)] Created.
APIがデプロイされました。
$ dacrane ls
base (base)
remote-latest (api-remote-image)
api-latest (api-image)
as (app-service)
api (local-docker)
実際にAPIの疎通確認をしてみます。
$ curl https://tikeda-sample-as.azurewebsites.net
hello world
これで全てのデプロイが完了です!
環境削除
最後にDacraneで作った環境を破棄します。破棄もインスタンスごとに行います。リソース間の依存関係に注意して順番に削除してください。
$ dacrane destroy as
$ dacrane destroy remote-latest
$ dacrane destroy api
$ dacrane destroy base
$ dacrane destroy api-latest
Dacraneのアンインストール
Dacraneもアンインストールされたい方は、次のコマンドでアンインストールできます。
$ make uninstall-plugin
$ make uninstall
参考
完全なコード
今回作成したデプロイメントコードの完全版です。
name: api-image # モジュール名
parameter: # モジュール引数
type: object
properties:
tag: { type: string, default: "latest" }
import: # 外部モジュールインポート
- https://raw.githubusercontent.com/SIOS-Technology-Inc/dacrane/0.0.7/module/docker-npm.yaml
modules: # 他モジュール、プラグイン呼び出し。
- name: api-local-image
module: docker-npm # npmでビルドされるdockerイメージを作るモジュールを呼び出す
argument:
image: sample-api
tag: ${{ parameter.tag }}
---
name: local-docker
parameter:
type: object
properties:
image: { type: string, default: sample-api }
tag: { type: string, default: latest }
modules:
- module: docker/resource/container
name: api
argument:
name: api
image: ${{ parameter.image }}
tag: ${{ parameter.tag }}
port: 3000:3000
env:
- name: PORT
value: "3000"
---
name: base
parameter:
type: object
required: ["prefix"]
properties:
prefix: { type: string }
modules:
- name: rg
module: terraform/resource/azurerm_resource_group
argument:
provider: &config
features: {}
client_id: ${{ env.ARM_CLIENT_ID }}
client_secret: ${{ env.ARM_CLIENT_SECRET }}
tenant_id: ${{ env.ARM_TENANT_ID }}
subscription_id: ${{ env.ARM_SUBSCRIPTION_ID }}
resource:
name: ${{ parameter.prefix }}-sample-rg
location: "Japan East"
- name: acr
module: terraform/resource/azurerm_container_registry
argument:
provider: *config
resource:
name: ${{ parameter.prefix }}sampleacr
resource_group_name: ${{ modules.rg.name }}
location: "Japan East"
sku: Basic
admin_enabled: true
---
name: api-remote-image
parameter:
type: object
properties:
tag: { type: string, default: "latest" }
image: { type: string, default: "sample-api" }
acr: { type: object }
modules:
- name: api-remote-image
module: docker/resource/remote-image
argument:
image: ${{ parameter.image }}
tag: ${{ parameter.tag }}
remote:
url: ${{ parameter.acr.login_server }}
user: ${{ parameter.acr.admin_username }}
password: ${{ parameter.acr.admin_password }}
---
name: app-service
argument:
parameter:
type: object
required: ["base"]
properties:
base: { type: object }
tag: { type: string, default: "latest" }
image: { type: string, default: "sample-api" }
modules:
- name: asp
module: terraform/resource/azurerm_app_service_plan
argument:
provider:
features: {}
client_id: ${{ env.ARM_CLIENT_ID }}
client_secret: ${{ env.ARM_CLIENT_SECRET }}
tenant_id: ${{ env.ARM_TENANT_ID }}
subscription_id: ${{ env.ARM_SUBSCRIPTION_ID }}
resource:
name: ${{ parameter.base.parameter.prefix }}-sample-asp
resource_group_name: ${{ parameter.base.modules.rg.name }}
location: "Japan East"
kind: "Linux"
reserved: true
sku:
tier: Basic
size: B1
- name: as
module: terraform/resource/azurerm_app_service
argument:
provider:
features: {}
client_id: ${{ env.ARM_CLIENT_ID }}
client_secret: ${{ env.ARM_CLIENT_SECRET }}
tenant_id: ${{ env.ARM_TENANT_ID }}
subscription_id: ${{ env.ARM_SUBSCRIPTION_ID }}
resource:
name: ${{ parameter.base.parameter.prefix }}-sample-as
resource_group_name: ${{ parameter.base.modules.rg.name }}
location: "Japan East"
app_service_plan_id: ${{ modules.asp.id }}
site_config:
linux_fx_version: DOCKER|${{ parameter.base.modules.acr.login_server }}/${{ parameter.image }}:${{ parameter.tag }}
app_settings:
DOCKER_REGISTRY_SERVER_URL: ${{ parameter.base.modules.acr.login_server }}
DOCKER_REGISTRY_SERVER_USERNAME: ${{ parameter.base.modules.acr.admin_username }}
DOCKER_REGISTRY_SERVER_PASSWORD: ${{ parameter.base.modules.acr.admin_password }}
Dacraneサンプルコード
↓で今回の元ネタのサンプルコードを公開しています。こちらもよろしければご覧ください。
https://github.com/SIOS-Technology-Inc/dacrane/tree/0.0.7/example/app-services
まとめ
最後までお読みいただきありがとうございます!
ご覧いただいたとおりインフラからアプリまで全てをDacraneが全て管理できていることがお分かりいただけたかと思います。
GitHub StarやIssueで開発を応援ください!
まだまだ発展途上のOSSですが、少しでもいいなと思っていただいた方はGitHub Starで応援ください!また、意見や改善を思いついた方はIssueやPRをいただけると嬉しいです!