定期実行するAzure Container InstancesをAzure Resource Managerで定義しJenkins Pipelineでデプロイする

# 修正（2020/11/13）

Logic Appsからコンテナを起動・停止する際にHttpのアクションを使用して、別途`Microsoft.Authorization/roleAssignments`でロールを割り当てていた箇所を、ApiConnectionのアクションを使用して、`Microsoft.Web/connections`で接続するよう変更しました。

# はじめに

Azure Container Instancesは従量課金ですが、現状、起動や停止をスケジューリングする機能はついていないようなので、運用コストを減らすためにも定期実行させる仕組みが欲しいところです。

今回、Azure上でDockerコンテナを毎日5分間だけ立ち上げておき、その間だけリクエストを受け付けるようなニッチなアプリを稼働させたく、でも、どうせなら、きちんとコード管理し、継続デプロイできる仕組みにしたかったので、併せてやってみました。

# やりたいこと

- Azure上でDockerコンテナを毎日5分間だけ立ち上げたい
- Azure Resource Managerで定義し、コードとして管理したい
- Jenkins Pipelineでデプロイしたい

# 前提

- azure-cli 2.13.0
- リソースグループが既にあり、そのリソースグループの所有者権限を持っていること

# やってみる

## 前処理

まずはリソースグループ内にデプロイで必要な環境を整えます。今回、リソースグループは`my-group`とします。

- リソースに対するService Principalを作成

{SUBSCRIPTION_ID}にはサブスクリプションIDを指定します。また、権限の範囲を局所化するため、リソースに対して作成します。

```
az ad sp create-for-rbac --name "http://my-server" --role Owner --scopes /subscriptions/{SUBSCRIPTION_ID}/resourceGroups/my-group
```

以下が出力されるのでメモっておきます。（後でJenkinsにシークレットキーとして登録したりします。）

```
{
  "appId": "ユーザ名",
  "displayName": "my-server",
  "name": "http://my-server",
  "password": "パスワード",
  "tenant": "テナント"
}
```

- 作成したService Principalでログイン

```
az login --service-principal --username ユーザ名 --password パスワード --tenant テナント
```

- リソース内にACRのリポジトリを作成

今回リポジトリ名は`mygroupregistry0001`とします。（これは全体でユニークになる必要があるため、試す場合は別の名前にしてください。）

```
az acr create --name mygroupregistry0001 --resource-group my-group --sku Basic --location japaneast
```

## 構成

今回のコードベースは下記のような構成となります。

```
.
├── Dockerfile
├── Jenkinsfile
├── resources
│   └── template.json
└── scripts
    ├── build.sh
    └── deploy.sh
```

## Dockerfileの作成

とりあえず今回はnginxを起動します。

```Dockerfile
FROM nginx
EXPOSE 80
```

## Azure Resource Managerの定義ファイル（テンプレートファイル）作成

Azure Container Instancesの定義と、定期的にDockerコンテナの起動と停止を行うためのLogic Appsの定義を行います。

```resources/template.json
{
    "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "location": {
            "defaultValue": "[resourceGroup().location]",
            "type": "string"
        },
        "resource_group_name": {
            "defaultValue": "[resourceGroup().name]",
            "type": "string"
        },
        "subscription_id": {
            "defaultValue": "[subscription().subscriptionId]",
            "type": "string"
        },
        "username": {
            "type": "securestring"
        },
        "password": {
            "type": "securestring"
        },
        "registry_name": {
            "type": "string"
        },
        "version": {
            "type": "string"
        },
        "subdomain": {
            "type": "string"
        },
        "container_port": {
            "type": "int"
        },
        "containerGroups_server_name": {
            "defaultValue": "server",
            "type": "string"
        },
        "connections_aci_name": {
            "defaultValue": "aci",
            "type": "string"
        },
        "workflows_start_server_name": {
            "defaultValue": "start-server",
            "type": "string"
        },
        "workflows_stop_server_name": {
            "defaultValue": "stop-server",
            "type": "string"
        }
    },
    "variables": {},
    "resources": [
        {
            "type": "Microsoft.ContainerInstance/containerGroups",
            "apiVersion": "2019-12-01",
            "name": "[parameters('containerGroups_server_name')]",
            "location": "[parameters('location')]",
            "properties": {
                "sku": "Standard",
                "containers": [
                    {
                        "name": "[parameters('containerGroups_server_name')]",
                        "properties": {
                            "image": "[concat(parameters('registry_name'), '.azurecr.io/', parameters('containerGroups_server_name'), ':', parameters('version'))]",
                            "ports": [
                                {
                                    "protocol": "TCP",
                                    "port": "[parameters('container_port')]"
                                }
                            ],
                            "environmentVariables": [],
                            "resources": {
                                "requests": {
                                    "memoryInGB": 0.5,
                                    "cpu": 1
                                }
                            }
                        }
                    }
                ],
                "initContainers": [],
                "imageRegistryCredentials": [
                    {
                        "server": "[concat(parameters('registry_name'), '.azurecr.io')]",
                        "username": "[parameters('username')]",
                        "password": "[parameters('password')]"
                    }
                ],
                "restartPolicy": "OnFailure",
                "ipAddress": {
                    "ports": [
                        {
                            "protocol": "TCP",
                            "port": "[parameters('container_port')]"
                        }
                    ],
                    "type": "Public",
                    "dnsNameLabel": "[parameters('subdomain')]"
                },
                "osType": "Linux"
            }
        },
        {
            "type": "Microsoft.Web/connections",
            "apiVersion": "2016-06-01",
            "name": "[parameters('connections_aci_name')]",
            "location": "[parameters('location')]",
            "kind": "V1",
            "properties": {
                "displayName": "container_connect",
                "customParameterValues": {},
                "parameterValues": {
                    "token:clientId": "[parameters('username')]",
                    "token:clientSecret": "[parameters('password')]",
                    "token:TenantId": "[subscription().tenantId]",
                    "token:grantType": "client_credentials"
                },
                "api": {
                    "id": "[concat('/subscriptions/', parameters('subscription_id'), '/providers/Microsoft.Web/locations/', parameters('location'), '/managedApis/aci')]"
                }
            }
        },
        {
            "type": "Microsoft.Logic/workflows",
            "apiVersion": "2017-07-01",
            "name": "[parameters('workflows_start_server_name')]",
            "location": "[parameters('location')]",
            "dependsOn": [
                "[resourceId('Microsoft.ContainerInstance/containerGroups', parameters('containerGroups_server_name'))]"
            ],
            "properties": {
                "state": "Enabled",
                "definition": {
                    "$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#",
                    "contentVersion": "1.0.0.0",
                    "parameters": {
                        "$connections": {
                            "defaultValue": {},
                            "type": "Object"
                        }
                    },
                    "triggers": {
                        "Recurrence": {
                            "recurrence": {
                                "frequency": "Day",
                                "interval": 1,
                                "schedule": {
                                    "hours": [
                                        "10"
                                    ],
                                    "minutes": [
                                        30
                                    ]
                                },
                                "timeZone": "Tokyo Standard Time"
                            },
                            "type": "Recurrence"
                        }
                    },
                    "actions": {
                        "START_CONTAINER": {
                            "runAfter": {},
                            "type": "ApiConnection",
                            "inputs": {
                                "host": {
                                    "connection": {
                                        "name": "@parameters('$connections')['aci']['connectionId']"
                                    }
                                },
                                "method": "post",
                                "path": "[concat('/subscriptions/', parameters('subscription_id'), '/resourceGroups/', parameters('resource_group_name'), '/providers/Microsoft.ContainerInstance/containerGroups/', parameters('containerGroups_server_name'), '/start')]",
                                "queries": {
                                    "x-ms-api-version": "2019-12-01"
                                }
                            }
                        }
                    },
                    "outputs": {}
                },
                "parameters": {
                    "$connections": {
                        "value": {
                            "aci": {
                                "connectionId": "[resourceId('Microsoft.Web/connections', parameters('connections_aci_name'))]",
                                "connectionName": "aci",
                                "id": "[concat('/subscriptions/', parameters('subscription_id'), '/providers/Microsoft.Web/locations/', parameters('location'), '/managedApis/aci')]"
                            }
                        }
                    }
                }
            }
        },
        {
            "type": "Microsoft.Logic/workflows",
            "apiVersion": "2017-07-01",
            "name": "[parameters('workflows_stop_server_name')]",
            "location": "[parameters('location')]",
            "dependsOn": [
                "[resourceId('Microsoft.ContainerInstance/containerGroups', parameters('containerGroups_server_name'))]"
            ],
            "identity": {
                "type": "SystemAssigned"
            },
            "properties": {
                "state": "Enabled",
                "definition": {
                    "$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#",
                    "contentVersion": "1.0.0.0",
                    "parameters": {
                        "$connections": {
                            "defaultValue": {},
                            "type": "Object"
                        }
                    },
                    "triggers": {
                        "Recurrence": {
                            "recurrence": {
                                "frequency": "Day",
                                "interval": 1,
                                "schedule": {
                                    "hours": [
                                        "10"
                                    ],
                                    "minutes": [
                                        35
                                    ]
                                },
                                "timeZone": "Tokyo Standard Time"
                            },
                            "type": "Recurrence"
                        }
                    },
                    "actions": {
                        "STOP_CONTAINER": {
                            "runAfter": {},
                            "type": "ApiConnection",
                            "inputs": {
                                "host": {
                                    "connection": {
                                        "name": "@parameters('$connections')['aci']['connectionId']"
                                    }
                                },
                                "method": "post",
                                "path": "[concat('/subscriptions/', parameters('subscription_id'), '/resourceGroups/', parameters('resource_group_name'), '/providers/Microsoft.ContainerInstance/containerGroups/', parameters('containerGroups_server_name'), '/stop')]",
                                "queries": {
                                    "x-ms-api-version": "2019-12-01"
                                }
                            }
                        }
                    },
                    "outputs": {}
                },
                "parameters": {
                    "$connections": {
                        "value": {
                            "aci": {
                                "connectionId": "[resourceId('Microsoft.Web/connections', parameters('connections_aci_name'))]",
                                "connectionName": "aci",
                                "id": "[concat('/subscriptions/', parameters('subscription_id'), '/providers/Microsoft.Web/locations/', parameters('location'), '/managedApis/aci')]"
                            }
                        }
                    }
                }
            }
        }
    ],
    "outputs": {}
}
```

nginxのDocker Imageをコンテナとして稼働させるContainer Instanceと、10時30分にコンテナを起動するLogic Apps、10時35分にコンテナを停止するLogic Appsを定義しています。

また、Container InstanceとLogic Appsとの連携は`Microsoft.Web/connections`を使用しています。

## ビルドとデプロイのスクリプト作成

```scripts/build.sh
#!/bin/bash

REGISTRY_NAME=$1
SERVER_NAME=$2
VERSION=$3

docker build --tag ${REGISTRY_NAME}.azurecr.io/${SERVER_NAME}:${VERSION} .
```

ここでは単にDocker Imageを作成しています。

```scripts/deploy.sh
#!/bin/bash

REGISTRY_NAME=$1
SERVER_NAME=$2
VERSION=$3
AZ_USERNAME=$4
AZ_PASSWORD=$5
AZ_TENANT=$6
RESOURCE_GROUP=$7
SUBDOMAIN=$8
CONTAINER_PORT=$9

az login --service-principal --username ${AZ_USERNAME} --password ${AZ_PASSWORD} --tenant ${AZ_TENANT}
az acr login --name ${REGISTRY_NAME}
docker push ${REGISTRY_NAME}.azurecr.io/${SERVER_NAME}:${VERSION}
az deployment group create --resource-group ${RESOURCE_GROUP} --template-file resources/template.json --parameters username=${AZ_USERNAME} password=${AZ_PASSWORD} registry_name=${REGISTRY_NAME} containerGroups_server_name=${SERVER_NAME} version=${VERSION} subdomain=${SUBDOMAIN} container_port=${CONTAINER_PORT}
az container stop --resource-group ${RESOURCE_GROUP} --name ${SERVER_NAME}
```

ビルドされたDocker ImageをACRにプッシュし、先ほど作成したARMのテンプレートを指定してデプロイしています。

また、デプロイ時、コンテナが起動状態となるため、停止処理も入れています。

## Jenkinsfileの作成

```Jenkinsfile
pipeline {
  agent any

  environment {
    GIT_COMMIT_HASH = """${sh (
                  returnStdout: true,
                  script: "git log -n 1 --pretty=format:'%H'"
                )}"""
    DATETIME = """${sh(
                  returnStdout: true,
                  script: "echo `date +%Y%m%d%H%M%S`"
                )}"""
    RESOURCE_GROUP = 'my-group'
    REGISTRY_NAME = 'mygroupregistry0001'
    SERVER_NAME = 'server'
    SUBDOMAIN = 'ユニークなサブドメイン'
    CONTAINER_PORT = 80
    VERSION = """${sh (
                  returnStdout: true,
                  script: "git log -n 1 --pretty=format:'%H'"
                )}"""
    AZ_USERNAME = credentials('az_username')
    AZ_PASSWORD = credentials('az_password')
    AZ_TENANT = credentials('az_tenant')    
  }

  stages {
    stage('Build') {
      steps {
        echo "Building.. [${GIT_COMMIT_HASH}] [${DATETIME}]"
        sh "/bin/sh scripts/build.sh ${REGISTRY_NAME} ${SERVER_NAME} ${VERSION}"
      }
    }
    stage('Test') {
      steps {
        echo 'Testing..'
      }
    }
    stage('Deploy') {
      steps {
        echo 'Deploying....'
        sh "/bin/sh scripts/deploy.sh ${REGISTRY_NAME} ${SERVER_NAME} ${VERSION} ${AZ_USERNAME} ${AZ_PASSWORD} ${AZ_TENANT} ${RESOURCE_GROUP} ${SUBDOMAIN} ${CONTAINER_PORT}"
      }
    }
  }
}
```

Jenkinsfileでは先ほど作成したビルドとデプロイのスクリプトを呼んでいるだけのシンプルな記述になっています。

## Jenkinsでジョブを作成

最後にJenkinsでデプロイするためのジョブの作成を行います。

デプロイ時にAzure環境にService Principalでログインする必要があるので、まずは、「Jenkinsの管理」→「Manage Credentials」よりJenkinsのSecret textに下記の３つのIDを登録します。

az_username
az_password
az_tenant

上記のパスワードにはそれぞれ最初にメモっていた「ユーザ名」「パスワード」「テナント」を設定します。

次にジョブをパイプラインで作成します。その際、パイプラインの定義を「Pipeline from script SCM」にしてGitのリポジトリから今回のコードをとってくるようにします。

## 実行＆動作確認

Jenkinsにて作成したジョブのビルドを実行すると、Azure環境のリソースグループにデプロイされます。

デプロイ時はコンテナは停止するようにしているので、動作確認はLogic Appsのstart-serverをトリガー実行し、コンテナが起動して ブラウザでアクセスできることを確認します。その後、stot-serverをトリガー実行し、停止することが確認できれば、成功です。

このままだと、毎日10時30分から10時35分の間コンテナが稼働することになりますので、リソースを消して後片付けしておいてください。

# さいごに

何かしら定期実行を行いたい場合、AzureであればAzure Functionsが第一候補になるのだと思いますが、開発言語やらでいろいろ制約受けるので、できればコンテナで動かしたい、でもコストを抑えたい、といったときにニーズがあるかもです。

今回はJenkinsで動かしていますが、Jenkinsfileで宣言的に記述できるようになったので、Azure PipelineやCircleCI、GitHub Actionsでも似たような感じでCI/CDできそうです。

Azureを宣言的にリソース管理するケースでのドキュメントが公式ドキュメント以外は少なく、AWSに比べるとなかなか大変でした。。。