Alibaba Cloud サーバレスで作るTerraform実行ポータル【後編】

2023年3月30日掲載

キービジュアル

皆さまこんにちは、ソフトバンクの周です。

この記事では非エンジニアの方々でも使えるTerraform実行ポータルを作ってみましたのでご紹介させていただきます。

本記事は"Alibaba Cloud サーバレスで作るTerraform実行ポータル"記事の後編になります。前編記事もあわせてご覧下さい。

目次

アプリケーションの準備

Terraform

次にデプロイするTerraformコードを作成していきます。本記事では全体のアーキテクチャを中心にご紹介するため、デプロイする環境はVPCのみの簡単な構成となります。

コードではterraform stateファイルは前編で作成した"backend-bucket-02"に格納するように指定しています。

mkdir backend && cd backend
touch main.tf

main.tfに以下のコードを貼り付けます。

variable "access_key" {}
variable "secret_key" {}
variable "region" { default = "ap-northeast-1" }

terraform {
  backend "oss" {
  }
}

// alibaba cloud provider
provider "alicloud" {
  access_key = var.access_key
  secret_key = var.secret_key
  region     = var.region
}

// create vpc
resource "alicloud_vpc" "default" {
  vpc_name   = "default"
  cidr_block = "192.168.1.0/24"
}

バックエンド (Node.js)

次にバックエンドのコードを作成していきます。本記事では`Node.js` + `Express`で作成していきます。

作成するディレクトリは`main.tf`と同じbackendフォルダになります。

npm init -y
npm i express dotenv child_process cors
touch server.js 

`server.js` ファイルが作られていますので、以下のソースコードを貼り付けます。

`/create`と`/destroy`APIを作成しており、それぞれ`child_process`で`terraform apply`と`terraform destroy`を実行しています。

実行に必要アクセスキーおよびシークレットキーは環境によって異なりますので、フロントから入力された値を使用します。

また、バケットにstateファイルを格納するため、コード内でアクセスキーの設定をお願いします。

'use strict';

const express = require('express');
const { execSync } = require('child_process')
require('dotenv')

// Constants
const PORT = 8080;
const HOST = '0.0.0.0';

const cors = require('cors')
const app = express();
const env = process.env

const BUCKET = 'state-bucket-03';
const ACCESS = 'Your Access Key'; // アクセスキーの設定、手動で設定します。
const SECRET = env.SECRET; // シークレットキーの設定、Function Computeから取得します。
const MOUNTPATH = '/app/backend';

app.use(express.json())
app.use(cors())

app.post('/create', (req, res) => {
  const accessKey = req.body.access_key;
  const secretKey = req.body.secret_key;
  const head = accessKey.slice(0, 8);

  const config = `bucket = \"${BUCKET}\"
key    = \"${head}/terraform.tfstate\"
access_key = \"${ACCESS}\"
secret_key = \"${SECRET}\"
region = \"ap-northeast-1\"`;

  const tfvars = `access_key = \"${accessKey}\"
secret_key = \"${secretKey}\"`;

  execSync(`echo '${config}' > ${MOUNTPATH}/${head}.tfbackend`, ['-e']);
  execSync(`echo '${tfvars}' > ${MOUNTPATH}/${head}.tfvars`, ['-e']);
  execSync(`terraform init -backend-config="${MOUNTPATH}/${head}.tfbackend"`);
  const stdout = execSync(`terraform apply -no-color -auto-approve -var-file="${MOUNTPATH}/${head}.tfvars"`)
  execSync(`rm ${MOUNTPATH}/${head}.tfvars`);
  res.send(stdout);
});

app.post('/destroy', (req, res) => {
  const accessKey = req.body.access_key;
  const secretKey = req.body.secret_key;
  const head = accessKey.slice(0, 8);

  const config = `bucket = \"${BUCKET}\"
key    = \"${head}/terraform.tfstate\"
access_key = \"${ACCESS}\"
secret_key = \"${SECRET}\"
region = \"ap-northeast-1\"`;

  const tfvars = `access_key = \"${accessKey}\"
secret_key = \"${secretKey}\"`;

  execSync(`echo '${config}' > ${MOUNTPATH}/${head}.tfbackend`, ['-e']);
  execSync(`echo '${tfvars}' > ${MOUNTPATH}/${head}.tfvars`, ['-e']);
  execSync(`terraform init -backend-config="${MOUNTPATH}/${head}.tfbackend"`);
  const stdout = execSync(`terraform destroy -no-color -auto-approve -var-file="${MOUNTPATH}/${head}.tfvars"`)
  execSync(`rm ${MOUNTPATH}/${head}.tfvars`);
  res.send(stdout);
});

app.listen(PORT, HOST, () => {
  console.log(`Running on http://${HOST}:${PORT}`);
});

Dockerイメージ

次に、作成したバックエンドとTerraformのコードをDockerイメージ化していきます。

作成するディレクトリは`main.tf`と同じbackendフォルダになります。

touch Dockerfile

`Dockerfile`ができていますので、以下のコードを貼り付けます。

FROM node:16.19

# install terraform
RUN wget https://releases.hashicorp.com/terraform/1.0.7/terraform_1.0.7_linux_amd64.zip && \
  unzip terraform_1.0.7_linux_amd64.zip && \
  mv terraform /usr/local/bin/ && \
  rm terraform_1.0.7_linux_amd64.zip

WORKDIR /app
COPY package.json /app
COPY server.js /app
COPY main.tf /app
RUN npm install 

EXPOSE 8080

CMD [ "node", "server.js" ]

フロントエンド (React)

最後に、フロントエンドのコードを作成します。本記事では`React`を使って作成します。

ルートディレクトリに移動後、`React`のアプリ作成と関連モジュールをインストールします。

npx create-react-app frontend
cd frontend
npm i @mui/material @emotion/react @emotion/styled react-terminal-ui axios

スタイリングには`Material-UI`と`react-terminal-ui`を使用しています。

`App.js`のコードは以下になります。

import React from 'react';
import { Box, AppBar, Toolbar, Typography, TextField, Button } from '@mui/material';
import Terminal, { ColorMode, TerminalOutput } from 'react-terminal-ui';
import axios from 'axios';

const Endpoint = 'Your Endpoint'; // Function Computeのエンドポイントを設定

function App() {

  const [terminalLineData, setTerminalLineData] = React.useState([
    <TerminalOutput></TerminalOutput>
  ]);
  const [accessKey, setAccessKey] = React.useState('');
  const [secretKey, setSecretKey] = React.useState('');
  const [isDisable, setIsDisable] = React.useState(false);

  const onDeploy = async () => {
    try {
      setIsDisable(true);
      const response = await axios.post(`${Endpoint}/create`, {
        access_key: accessKey,
        secret_key: secretKey
      }, {
        headers: {
          'Content-Type': 'application/json'
        }
      });
      setTerminalLineData([
        <TerminalOutput>{response.data}</TerminalOutput>
      ]);
    } catch (error) {
      setTerminalLineData([
        <TerminalOutput>デプロイに失敗しました。</TerminalOutput>
      ]);
    } finally {
      setIsDisable(false);
    }
  }

  const onRemove = async () => {
    try {
      setIsDisable(true);
      const response = await axios.post(`${Endpoint}/destroy`, {
        access_key: accessKey,
        secret_key: secretKey
      }, {
        headers: {
          'Content-Type': 'application/json'
        }
      });
      setTerminalLineData([
        <TerminalOutput>{response.data}</TerminalOutput>
      ]);
    } catch (error) {
      setTerminalLineData([
        <TerminalOutput>デプロイに失敗しました。</TerminalOutput>
      ]);
    } finally {
      setIsDisable(false);
    }
  }

  return (
    <Box sx={{ flexGrow: 1 }}>
      <AppBar component="nav">
        <Toolbar>
          <Typography variant="h6" component="div" sx={{ flexGrow: 1, display: { xs: 'none', sm: 'block' } }} >
            デプロイポータル
          </Typography>
        </Toolbar>
      </AppBar>

      <Box sx={{ display: 'flex' }}>
        <Box sx={{
          display: 'flex',
          justifyContent: 'center',
          alignItems: 'center',
          height: '100vh',
          flexDirection: 'column',
          minWidth: '50vw'
        }}>
          <TextField
            id="access-key"
            label="アクセスキー"
            variant="standard"
            sx={{
              paddingBottom: '10px',
              width: '300px'
            }}
            value={accessKey}
            onChange={(event) => setAccessKey(event.target.value)}
          />
          <TextField
            id="secret-key"
            label="シークレットキー"
            variant="standard"
            type='password'
            sx={{
              paddingBottom: '40px',
              width: '300px'
            }}
            value={secretKey}
            onChange={(event) => setSecretKey(event.target.value)}
          />
          <Box sx={{ display: 'flex' }}>
            <Box>
              <Button
                variant="contained"
                onClick={onDeploy}
                disabled={accessKey === '' || secretKey === '' || isDisable}>
                デプロイ
              </Button>
            </Box>
            <Box sx={{ paddingLeft: '50px' }}>
              <Button
                variant="contained"
                onClick={onRemove}
                disabled={accessKey === '' || secretKey === '' || isDisable}>
                削除
              </Button>
            </Box>
          </Box>
        </Box>
        <Box sx={{
          display: 'flex',
          justifyContent: 'center',
          alignItems: 'center',
          minHeight: '100vh',
          flexDirection: 'column',
          minWidth: '45vw'
        }}>
          <Terminal
            name='Deploy Status'
            colorMode={ColorMode.Dark}
          >
            {terminalLineData}
          </Terminal>
        </Box>
      </Box>
    </Box>
  );
}

export default App;

コード内でEndpointは前編で作成したFunction Computeのエンドポイントに書き換える必要があります。

作成したFunction Computeの関数から以下の部分を貼り付けてください。

以上で全てのコードの作成は完了になります。

アプリケーションのデプロイ

イメージプッシュ

まずは作成した`Dockerfile`を使って、`Terraform`と`Backend`のコードをDockerイメージ化します。

(MacOSのM1チップでビルドされたイメージは2023/3/15時点ではFunction Computeでサポートされていませんので、ご注意ください。)

cd ../backend
docker build --tag backend:latest .

ビルド後、イメージIDを確認しメモしておきます。

docker images | grep backend

先ほどメモしたイメージIDを置き換えて、下記コマンドでACRにプッシュします。

下記のサンプルでは東京リージョンを対象の手順となっていますので、ACRのリポジトリを別リージョンに作成した場合は、適宜リージョンを変更してください。

docker login registry-intl.ap-northeast-1.aliyuncs.com
docker tag <イメージID> registry-intl.ap-northeast-1.aliyuncs.com/portal-space/backend:latest
docker push registry-intl.ap-northeast-1.aliyuncs.com/portal-space/backend:latest

以上で、作成したバックエンド及びTerraformのコードのACRプッシュが完了しました。

Function Computeデプロイ

次にACRにプッシュしたイメージをFunction Computeにデプロイしていきます。手順は下記の通りです。

  1. Alibaba Cloud コンソールのプロダクト一覧より「Function Compute」を選択します。
  2. 「サービスと機能」から作成した「backend-service」を選択します。
  3. 左メニュー「関数」から作成した「backend-fc」を選択します。
  4. 「設定」タブを選択し、環境情報から「変更」を選択します。
  5. 「Container Registry のイメージを選択」を押下します。
  6. Container Registry リポジトリで「portal-space/backend」イメージを選択します。
  7. イメージバージョンで「latest」を選択します。
  8. リスニングポート: 8080
  9. 実行タイムアウト時間: 120
  1. 「設定」タブを選択し、環境変数から「変更」を選択します。
  2. フォームエディタを選択し、以下の変数を入力します。
  3. 変数名: SECRET
  4. 値: バックエンドからTerraform stateをOSSにデプロイするためのシークレットキー。

以上でFunction ComputeへのDockerイメージデプロイは完了となります。

フロントエンドアップロード

最後に、フロントエンドアプリケーションをOSSにデプロイしていきます。

フロントエンドをビルドし、buildフォルダが生成されていることを確認してください。

cd ./frontend
npm run build

OSSにアップロードする手順は以下になります。

  1. Alibaba Cloud コンソールにログインし、プロダクト一覧より「Object Storage Service」を選択します。
  2. バケットリストから「frontend-bucket-01」を押下し、オブジェクトの「アップロード」ボタンを押下します。
  1. 生成した`build`フォルダの全ファイルを転送し、「アップロード」ボタンを押下します。
  1. 「権限管理」から「ACL」を選択します。
  2. 「バケットACL」の設定ボタンを押下し、「公開読み取り」を押下します。
  3. 「設定ボタン」で変更を保存します。
  1. 「データ管理」から「静的ページ」を選択します。
  2. 「デフォルトのホームページ」で "index.html" を入力し「設定」ボタンを押下します。

以上で全ての作業は完了となります。

また、フロントエンドのエンドポイントはバケットの概要欄から確認することができますので、「インターネットアクセス」のバケットドメイン名をブラウザに入力していただくと、アプリケーションにアクセスできます。

さいごに

本記事ではTerraformをウェブポータルから実行できる方法を紹介させていただきました。アーキテクチャをメインに紹介させていただきましたが、Terraformのテンプレートの変更やフロントエンドを作り込むことによって、もっと面白いアプリケーションができるかもしれません。

最後までご覧いただきありがとうございました。

関連サービス

Alibaba Cloud

Alibaba Cloudは中国国内でのクラウド利用はもちろん、日本-中国間のネットワークの不安定さの解消、中国サイバーセキュリティ法への対策など、中国進出に際する課題を解消できるパブリッククラウドサービスです。

おすすめの記事

条件に該当するページがございません