terraformでec2上にmateデスクトップ環境を構築(Ubuntu20.04LTS)

## 1. 実行環境

- Windows10
- git for windows(必須ではないが、Windows上でbashなどを使うため)
- Terraform v0.14.3
- awscli(2.1.14)

前提条件として、`aws configure`を予め実行しておくなどして、自身のaws環境にawsコマンドでアクセスできるようにしておく。

また、今回terraformとawscliはscoopでインストールしている。

```cmd
scoop install aws terraform
```

scoopのインストールは例えば
https://scoop.sh/ (公式)
https://qiita.com/Dooteeen/items/12dc8fb14042888113d0
などを参照

## 2. 実行

### 2-1. 必要ファイルの作成

適当なディレクトリに以下のファイルを作成、配置

```:ファイル構成
$ tree
.
├── main.tf       # terraformファイル
├── provision.sh  # provisioning用スクリプト
└── set_env       # 環境変数設定、必須ではないが便利のため
```

各ファイルの詳細：

#### 2-1-1. `main.tf`

```tf:main.tf
# 実行環境ごとに変わるものや秘匿したい情報は変数化出来る
variable "key_name" {}
variable "public_key_path" {}
variable "private_key_path" {}

provider "aws" {
  profile = "default"
  region  = "ap-northeast-1"
}

data "aws_ami" "ubuntu" {
  most_recent = true

  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"]
  }

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }

  owners = ["099720109477"] # Canonical
}

resource "aws_instance" "example" {
  ami = data.aws_ami.ubuntu.id
  instance_type               = "t3a.small"
  key_name                    = aws_key_pair.deployer.id
  associate_public_ip_address = true
  vpc_security_group_ids      = ["${aws_security_group.default.id}"]
  tags = {
    Name = "terraform-ubuntu-mate-test1"
  }
  provisioner "file" {
    source      = "provision.sh"
    destination = "/tmp/provision.sh"
    connection {
      type        = "ssh"
      host        = self.public_ip
      user        = "ubuntu"
      private_key = file(var.private_key_path)
    }
  }
  provisioner "remote-exec" {
    inline = [
      "chmod +x /tmp/provision.sh",
      "/tmp/provision.sh",
    ]

    connection {
      type        = "ssh"
      host        = self.public_ip
      user        = "ubuntu"
      private_key = file(var.private_key_path)
    }
  }
}

resource "aws_key_pair" "deployer" {
  key_name = var.key_name
  public_key = file(var.public_key_path)
}

resource "aws_security_group" "default" {
  name        = "ssh_default_security_group"
  description = "ssh default"

  # SSH access from anywhere
  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}
```

`main.tf`作成における主な参考リンク：

- Ubuntu20.04のAMIイメージの利用
  * https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/instance
- tagの利用(立ち上げるEC2インスタンスに"terraform-ubuntu-mate-test1"という名前をつけている)
  * https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ec2_tag
- 変数の使用(実行環境に依存性のある値や秘匿すべき情報などを外部から与える)
  * https://www.terraform.io/docs/commands/environment-variables.html#tf_var_name
  * https://dev.classmethod.jp/articles/terraform-getting-started-with-aws/
- EC2へsshでアクセスするためのキーペアの作成・管理
  * https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/key_pair
  * https://kenzo0107.hatenablog.com/entry/2017/03/27/215941
- セキュリティグループの作成・管理
  * https://dev.classmethod.jp/articles/my-mistake-about-creating-sg-by-terraform/
      - アウトバウンドは全部許可、インバウンドは22ポートによるsshのみ許可
- provisioningのスクリプトのアップロードと実行
  * https://www.terraform.io/docs/provisioners/remote-exec.html

秘密鍵・公開鍵関係の情報は外部変数として宣言しておき、環境変数`TF_VAR_**`を設定することでterraform側に渡すようにしている。(後で`source set_env`して読み込む)
また、ubuntuのprovisioningは`provision.sh`という別のスクリプトファイルにしておき、ローカルからEC2側へアップロード・実行させている。

(AWSのWebコンソール上からEC2を普通に作るのと比べると、はじめは一つ一つ記述しないといけないため思った以上に面倒だった。。。)

**注意点として、デスクトップ環境構築のためにインスタンスタイプは`t3a.small`タイプを指定しているため無料枠の対象外となっている。(`t3a.small`にあまり深い意味は無いが、`t2.micro`のメモリ1GBでは流石にスペックが足りないため)**

#### 2-1-2. `provision.sh`

```sh:provision.sh
#!/usr/bin/env sh
set -eux

# クラウド側のセットアップ完了を待たないとapt-get等が失敗する
while [ ! -f /var/lib/cloud/instance/boot-finished ]; do
    sleep 1
done

sudo apt-get update

# mate-desktop
sudo apt-get -y install \
    mate-desktop \
    mate-desktop-environment \
    mate-desktop-environment-extra \
    mate-session-manager

# xrdp
sudo apt-get install -y xrdp
sudo sed -e 's/^new_cursors=true/new_cursors=false/g' \
  -i /etc/xrdp/xrdp.ini
sudo service xrdp start

# xrdp xsession
echo "mate-session" > ~/.xsession
XDG_DATA_DIRS=/usr/share/mate:/usr/share/mate:/usr/local/share
XDG_DATA_DIRS=${XDG_DATA_DIRS}:/usr/share:/var/lib/snapd/desktop
cat <<EOF > ~/.xsessionrc
export XDG_SESSION_DESKTOP=mate
export XDG_DATA_DIRS=${XDG_DATA_DIRS}
export XDG_CONFIG_DIRS=/etc/xdg/xdg-mate:/etc/xdg
EOF

# set password
sudo USER_PASSWD=${USER_PASSWD:-changeit} sh -c '
  echo "${USER_PASSWD}\n${USER_PASSWD}" | passwd ubuntu
'

echo "Done: $(basename $0)"
```

mate-desktopのセットアップは大まかに
https://www.hiroom2.com/2018/05/06/ubuntu-1804-mate-ja/
https://www.hiroom2.com/2018/05/07/ubuntu-1804-xrdp-mate-ja/
を参考にして、`xrdp`によってリモートデスクトップを使えるようにしている。

特に詰まったポイントは、

```sh
while [ ! -f /var/lib/cloud/instance/boot-finished ]; do
    sleep 1
done
```
の部分。
どうやら「provisioningが始まるタイミング=クラウド側の環境の準備完了」ではないらしく、この部分を入れないと`apt-get update`でリポジトリ情報が更新されず、その結果`apt-get install`でエラーが起こってしまう。
（参考: https://stackoverflow.com/questions/42279763/why-does-terraform-apt-get-fail-intermittently ）

ここで、ログインユーザーはデフォルトで用意されている`ubuntu`にしており、ログインパスワードはとりあえず`changeit`でハードコードしていることに注意。

#### 2-1-3. `set_env`

先述のterraformファイルで外部変数を使用するために必要な環境変数を設定：

```sh:set_env
export TF_VAR_key_name=key_terraform
export TF_VAR_public_key_path="${HOME}/.ssh/key_terraform.pub"
export TF_VAR_private_key_path="${HOME}/.ssh/key_terraform"
```

上記の内容は適宜書き換えを行う。また、また、該当するパスに秘密鍵と公開鍵を予め作っておく。上記の例であれば、

```sh
ssh-keygen -t rsa -b 4096 -f $HOME/.ssh/key_terraform # 4096bitのRSA, ファイル名と置き場所も指定しておく
```
のようにすればOK
(terraformを実行するとAWS環境にアップロードされて、自動でキーペアが作成・管理されるようになる)

なお、今回は[git for windows](https://gitforwindows.org/)に付属のbash(msys2)を使っており、linuxと同様のやり方で動くようにしている。
（コマンドプロンプトなどを使う場合は少しやり方を変える必要）

### 2-2. ec2の起動とrdp接続

[2-1.](#2-1-必要ファイルの作成) で作ったファイルを置いたディレクトリ上でgit bashを立ち上げ、以下を実行すると起動

```sh
# 環境変数の設定(※パスした場合は、対話形式で変数を設定することも可能)
source set_env

# ディレクトリを初期化
terraform init

# awsリソースの起動
terraform apply   # 問題無いか聞かれるので、確認してOKなら`yes`を入力してEnterを押すとスタートする
```

provisioningには20~30分オーダーで時間がかかる。
無事完了した後は、

```sh
terraform show
```
で立ち上がったリソースの情報を確認出来るので、そこから`public_id`か`public_dns`を調べる(ssh接続に必要。他にはAWSコンソールのEC2ダッシュボード等からも確認出来るのでそれでもOK)

次に立ち上がったEC2へのリモートデスクトップ接続を行う。
今回はセキュリティを考えてEC2はssh用のポート(22)のみを開放しているため、ポートフォワードを行う必要がある。
そこで、git bash上で下記のようにssh接続を行う：

```sh
ssh -i ~/.ssh/key_terraform ubuntu@ec2-XX-XX-XX-XX.ap-northeast-1.compute.amazonaws.com -L 13389:localhost:3389
```

- `-i`オプションで使用する秘密鍵を指定している(実際に使っているものに合わせて適宜書き換え)
- `ubuntu@***`の`***`部分には上記で調べたEC2のIPアドレスかURL(`public_id`ないし`public_dns`部分)を入れる
- `-L`オプションでポートフォワードを行っており、ここではEC2側の3389ポート(リモートデスクトップ用の`xrdp`で使用)をローカル端末の13389ポートに転送している(ここも使用状況に合わせて適宜書き換え)

ここで、ウィンドウズキーを押して`rdp`と入力して検索を行うと「リモートデスクトップ接続」を選択出来るので、Enterを押して選択し、「コンピュータ名」に`localhost:13389`を入力して「接続」を押す。

![rdp.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/680173/db89a83e-7f10-c2c1-db01-5677de5663b0.png)


以下の画面が出てきたらusernameに`ubuntu`、passwordに`provision.sh`で設定したパスワードを入力してOKを押す。

![rdp_connect.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/680173/cb05a86e-c0b4-e49e-358e-fbf72f70ffa0.png)

以下のような画面が出てくればOK

![rdp_desktop.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/680173/4dd55cc0-a70e-a482-b147-c70db522f519.png)

実行にはお金がかかるので、使わないときはAWSコンソールからインスタンスを停止したり、
削除するときは

```sh
terraform destroy # 消してOKか聞かれるので、確認してからyesを入力
```
によって関連するAWSリソースをひとまとめに削除出来る


## (参考)日本語環境

上記までだと日本語用の設定が行われていないので、必要に応じてprovisioningの段階で設定を行う

例えば、以下のような内容を`provision.sh`に追記することで、日本語用パッケージ(IMEなど)のインストールやタイムゾーン・local設定などを行える：

```sh
apt install -y \
    language-pack-ja-base language-pack-ja \
    ibus-kkc ibus-mozc

# ubuntu-defaults-ja バージョンに応じて適切なものを選択・インストール
UBUNTU_VERSION=$(. /etc/lsb-release; echo $DISTRIB_RELEASE)
UBUNTU_CODENAME=$(. /etc/lsb-release; echo $DISTRIB_CODENAME)
if [ $UBUNTU_VERSION = "20.10" ]; then
    echo $UBUNTU_CODENAME
    wget https://www.ubuntulinux.jp/ubuntu-jp-ppa-keyring.gpg -P /etc/apt/trusted.gpg.d/
    wget https://www.ubuntulinux.jp/ubuntu-ja-archive-keyring.gpg -P /etc/apt/trusted.gpg.d/
    wget https://www.ubuntulinux.jp/sources.list.d/groovy.list -O /etc/apt/sources.list.d/ubuntu-ja.list
elif [ $UBUNTU_VERSION = "20.04" ]; then
    echo $UBUNTU_CODENAME
    wget -q https://www.ubuntulinux.jp/ubuntu-ja-archive-keyring.gpg -O- | apt-key add -
    wget -q https://www.ubuntulinux.jp/ubuntu-jp-ppa-keyring.gpg -O- | apt-key add -
    wget https://www.ubuntulinux.jp/sources.list.d/focal.list -O /etc/apt/sources.list.d/ubuntu-ja.list
elif [ $UBUNTU_VERSION = "18.04" ]; then
    echo $UBUNTU_CODENAME
    wget -q https://www.ubuntulinux.jp/ubuntu-ja-archive-keyring.gpg -O- | apt-key add -
    wget -q https://www.ubuntulinux.jp/ubuntu-jp-ppa-keyring.gpg -O- | apt-key add -
    wget https://www.ubuntulinux.jp/sources.list.d/bionic.list -O /etc/apt/sources.list.d/ubuntu-ja.list
else
    :
fi
apt-get -y update
apt-get -y upgrade
apt-get -y install ubuntu-defaults-ja

# locale
update-locale LANG=ja_JP.UTF8

# manual, if need
apt-get -y install manpages-ja manpages-ja-dev

# timezone
TIMEZONE="Asia/Tokyo"
echo $TIMEZONE > /etc/timezone
cp /usr/share/zoneinfo/${TIMEZONE} /etc/localtime   # This sets the time
```

日本語環境構築の参考：

- http://ubuntulinux.jp/japanese
- https://www.yokoweb.net/2018/05/04/ubuntu-18_04-lts-server-japanese/
- https://askubuntu.com/questions/323131/setting-timezone-from-terminal
- https://askubuntu.com/questions/3375/how-to-change-time-zone-settings-from-the-command-line
- https://serverfault.com/questions/94991/setting-the-timezone-with-an-automated-script

## その他の参考リンク

- https://medium.com/avmconsulting-blog/provisioning-aws-infrastructure-with-terraform-6ab885fb3fcb
- http://yargry.com/notebook/xrdp.html
- https://code.hounen-mansaku.com/archives/251