Infrastructure as Codeによるインフラ構築
Infrastructure as Codeとは
インフラ基盤をいわゆるプログラミングを書くかのようにコードで構築することをInfrastructure as Code(IaC)と呼びます。従来のインフラ基盤の構築といえば、大きいサーバのセットアップやLANケーブル接続によるネットワーク構築など物理的な設定が主な仕事でしたが、クラウドサービス全盛の現在ではそういった仕事が減っていき、PCの画面上から自由にインフラリソースの設定が行えることが可能になりました。
しかし、手軽に構築できるようになりますとスタートアップ企業でも何台ものサーバを扱えるということになり、これを1台1台手動で設定するのは大変な手間となります。その上、インフラ基盤の構築は属人化しやすく前任者が抜けた場合、手順書の更新が滞っていると混乱をきたしてしまいます。
IaCはこのような煩わしいインフラ基盤の構築、設定をコード化することで汎用的に誰にでも使いやすくすることができるといった概念ということになります。
Infrastructure as Codeに関してはこちらのオライリーの本に詳しいことが書かれていますので、ご興味のあるかたは手にとって読んでみてください。
IaCを体験してみる
インフラ構築をコードで行うにあたって色々なツールがあります。
今回私が行うのはTerraformを使ってAWSのEC2インスタンスを構築し、Ansibleを使ってWordPressを導入し、出来上がったものをAWS-CLIでAMI化するという一連の流れを行っていきます。また今回の構築結果はGitHubにまとめてあります。
使用するツール
Terraform v0.12.28
Ansible 2.9.10
AWS CLI 2.0.8
Terraformについて
Terraformはインフラ基盤の構築をコード上で起動してくれるまさにIaCを代表するインフラストラクチャ定義ツールの一つです。
クラウド以外のサービスでも使えますが、今現在はもっぱらAWSやGCPなどに代表されるパブリッククラウドリソースを定義して、構築してくれることに使われることが多いです。まずはTerraformを使ってAWSのEC2インスタンスを構築していきます。
EC2インスタンス構築
terraformはHCLという独自の言語を使ってインフラの構成定義を記述します。最初にterraform initで初期設定を行った後、定義構成ファイルを作成します。EC2インスタンスを1台構築するファイルはこちらになります。
create-ec2.tf
provider "aws" {
profile = "default" #AWS configureのデフォルト設定
region = "ap-northeast-1" #東京リージョン
}
resource "aws_instance" "YutaInstance" {
ami = "ami-06ad9296e6cf1e3cf" #Amazon Linux 2
instance_type = "t2.micro"
vpc_security_group_ids = ["sg-0b2cbd5fbd6f61d86"] #HTTPとSSH通信を許可
tags = {
Name = "YutaInstance-terraform"
}
key_name = "TokyoKeyPair" #キーペア
}
ファイル作成後に、terraform applyを実行するとTerraformがEC2インスタンスの構築を開始してくれます。
$ terraform apply
An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
# aws_instance.YutaInstance will be created
+ resource "aws_instance" "YutaInstance" {
+ ami = "ami-06ad9296e6cf1e3cf"
+ arn = (known after apply)
+ associate_public_ip_address = (known after apply)
+ availability_zone = (known after apply)
+ cpu_core_count = (known after apply)
+ cpu_threads_per_core = (known after apply)
+ get_password_data = false
+ host_id = (known after apply)
+ id = (known after apply)
+ instance_state = (known after apply)
+ instance_type = "t2.micro"
+ ipv6_address_count = (known after apply)
+ ipv6_addresses = (known after apply)
+ key_name = "TokyoKeyPair"
+ network_interface_id = (known after apply)
+ outpost_arn = (known after apply)
+ password_data = (known after apply)
+ placement_group = (known after apply)
+ primary_network_interface_id = (known after apply)
+ private_dns = (known after apply)
+ private_ip = (known after apply)
+ public_dns = (known after apply)
+ public_ip = (known after apply)
+ security_groups = (known after apply)
+ source_dest_check = true
+ subnet_id = (known after apply)
+ tags = {
+ "Name" = "YutaInstance-terraform"
}
+ tenancy = (known after apply)
+ volume_tags = (known after apply)
+ vpc_security_group_ids = [
+ "sg-0b2cbd5fbd6f61d86",
]
+ ebs_block_device {
+ delete_on_termination = (known after apply)
+ device_name = (known after apply)
+ encrypted = (known after apply)
+ iops = (known after apply)
+ kms_key_id = (known after apply)
+ snapshot_id = (known after apply)
+ volume_id = (known after apply)
+ volume_size = (known after apply)
+ volume_type = (known after apply)
}
+ ephemeral_block_device {
+ device_name = (known after apply)
+ no_device = (known after apply)
+ virtual_name = (known after apply)
}
+ metadata_options {
+ http_endpoint = (known after apply)
+ http_put_response_hop_limit = (known after apply)
+ http_tokens = (known after apply)
}
+ network_interface {
+ delete_on_termination = (known after apply)
+ device_index = (known after apply)
+ network_interface_id = (known after apply)
}
+ root_block_device {
+ delete_on_termination = (known after apply)
+ device_name = (known after apply)
+ encrypted = (known after apply)
+ iops = (known after apply)
+ kms_key_id = (known after apply)
+ volume_id = (known after apply)
+ volume_size = (known after apply)
+ volume_type = (known after apply)
}
}
Plan: 1 to add, 0 to change, 0 to destroy.
Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value: yes
aws_instance.YutaInstance: Creating...
aws_instance.YutaInstance: Still creating... [10s elapsed]
aws_instance.YutaInstance: Still creating... [20s elapsed]
aws_instance.YutaInstance: Creation complete after 22s [id=i-01c1da95774b288
known after applyと書いてある通り、いくつかの設定はTerraform側で自動で設定してくれる箇所もありますので、記述する量が少なく済むのもTerraformの良いポイントです。(もちろん構成定義ファイル内に記述すれば自分で設定することも可能です)
AnsibleによるWordPress導入
Ansibleの説明をしっかり説明したことがありませんでしたので、簡単に説明します。
AnsibleはRedHat社が開発している構成管理ツールと呼ばれるものです。
Ansibleを使うことで複数台のサーバに同一の設定をまとめて行えたり、ネットワーク構成の構築をコードで記述することが可能になります。ここでは、Terraformで構築したEC2インスタンスに対してWordPressを導入していこうと思います。
WordPressの構築のこちらの書籍を参考にしましたが、書籍内ではvagrantを使ってCentOS7の仮想マシン内に対してAnsibleを実施しており、Amazon Linux2で同じものを使うとハマってしまうところがありましたので、そこを中心に構築結果を記載していきます。
PHP-FPMのインストール
PHP-FPMの説明は簡単なものに留めますが、言ってしまえばWordPressなど高負荷なサイトで役に立つ追加機能を用意してくれるGCI(インターフェース)です。WordPressではPHPで構成されていますので、PHP関連のモジュールを入れる必要があるのですが、Amazon Linux2にデフォルトで入っているPHPのバージョンが5系と古く、現行のWordPressの推奨バージョン7.3を満たしていません。なのでまずはPHPのバージョンアップを行う必要があります。CentOSでしたらRemiレポジトリをインストールして、RemiからPHPとPHP-FPM関連のモジュールをインストールするのですが、Amazon Linux2ではExtras Libraryという専用のパッケージ群が用意されており、ここからPHPやNginxなどのソフトウェアをインストールする必要が出てきます。
CentOS7時のPHP-FPM関連モジュールインストールPlaybook
---
- name: Remiリポジトリをインストール
yum:
name: https://rpms.remirepo.net/enterprise/remi-release-7.rpm
- name: php-fpmと関連モジュールをインストール
yum:
name: "{{ item }}"
enablerepo: "remi-php{{ php_fpm_php_version|replace('.', '') }}"
with_items:
- php-fpm
- php-devel
- php-enchant
- php-mbstring
- php-process
- php-xml
- php-gd
- "{{ php_fpm_additional_packages }}"
notify:
- PHP-FPMを再起動
- name: PHP-FPMの起動
service:
name: php-fpm
state: started
enabled: true
Amazon Linux2時のPHPのバージョンアップとPHP-FPM関連モジュールインストールPlaybook
---
- name: amazon-linux-extrasでPHP関連モジュールをインストール
shell: "amazon-linux-extras enable php7.3"
changed_when: False
- name: php-fpmと関連モジュールをインストール
yum:
name: "{{ item }}"
enablerepo: "amzn2extra-php7.3"
with_items:
- php-fpm
- php-devel
- php-enchant
- php-mbstring
- php-process
- php-xml
- php-gd
- "{{ php_fpm_additional_packages }}"
notify:
- PHP-FPMを再起動
- name: PHP-FPMの起動
service:
name: php-fpm
state: started
enabled: true
大きな違いは最初にRemiレポジトリをインストールする部分をshellモジュールを使って直接amazon-linux-extrasコマンドを叩いて、PHPのバージョンを7.3に上げた後、amzn2extras-php7.3というレポジトリを有効化してPHP-FPM関連のモジュールをインストールしています。これと同じことをNginxでも行う必要があります。
Nginxをインストール
NginxのインストールもCentOSでしたらYumモジュールを使って簡単にインストールできたのですが、Amazon Linux2では上手く行きませんでした。
CentOS7時のNginxインストールPlaybook
---
- name: libselinux-pythonとEPELレポジトリをインストール
yum:
name: ['libselinux-python', 'epel-release'] # loop will be removed in version 2.11
state: present
with_items:
- libselinux-python
- epel-release
- name: Nginxをインストール
yum:
name: nginx
state: present
- name: Nginxを起動
service:
name: nginx
state: started
enabled: true
- name: nginx.conf テンプレート展開
template:
src: nginx.j2.conf
dest: /etc/nginx/nginx.conf
validate: nginx -t -c %s
notify:
- Nginxをリロード
Amazon Linux2時のNginxインストールPlaybook
---
- name: libselinux-pythonをインストール
yum:
name: libselinux-python
state: present
with_items:
- libselinux-python
- name: amazon-linux-extrasでNginxをインストールを有効化
shell: "amazon-linux-extras enable nginx1"
changed_when: False
- name: Nginxをインストールする
yum:
name: nginx
enablerepo: amzn2extra-nginx1
state: present
- name: Nginxを起動
service:
name: nginx
state: started
enabled: true
- name: nginx.conf テンプレート展開
template:
src: nginx.j2.conf
dest: /etc/nginx/nginx.conf
# validate: nginx -t -c %s
notify:
- Nginxをリロード
CentOS側のPlaybookは冒頭にEPELという外部レポジトリをインストールした上でNginxをインストールしていますが、Amazon Linux2ではEPELのインストールができず、こちらもExtaras LibraryからNginxをインストールすることにしました。enablerepo: amzn2extras-nginx1とExtras Library経由でNginxをインストールしていることがわかると思います。また下の方にNginxの構文チェックを行う、nginx -t -c %sがありますが、こちら何故かAmazon Linux2では失敗してしまい先に進むことができませんでした。
TASK [nginx.conf テンプレート展開] ************************************************************************************************
fatal: [18.183.117.108]: FAILED! => {"changed": false, "checksum": "1e48ac10f890d94be15896bad399b63d7bd8f477", "exit_status": 1, "msg": "failed to validate", "stderr": "nginx: [emerg] open() \"/home/ec2-user/.ansible/tmp/ansible-tmp-1595041315.8415804-24905-114659559593049/fastcgi_params\" failed (2: No such file or directory) in /etc/nginx/default.d/php.conf:12\nnginx: configuration file /home/ec2-user/.ansible/tmp/ansible-tmp-1595041315.8415804-24905-114659559593049/source test failed\n", "stderr_lines": ["nginx: [emerg] open() \"/home/ec2-user/.ansible/tmp/ansible-tmp-1595041315.8415804-24905-114659559593049/fastcgi_params\" failed (2: No such file or directory) in /etc/nginx/default.d/php.conf:12", "nginx: configuration file /home/ec2-user/.ansible/tmp/ansible-tmp-1595041315.8415804-24905-114659559593049/source test failed"], "stdout": "", "stdout_lines": []}
ファイルが存在しないと書かれていますが、EC2へSSH接続して直接コマンド実行すれば問題なく動くことは確認できて原因が不明でした。対処療法にはなりますが、この部分のみをコメントアウトすることで次の処理に問題なく進むことができました。すべての修正が完了し、ansible-playbook wordpress.ymlとコマンドを実行すると無事にWordPressのインストール画面が開くことを確認できました。
AMIの作成
次にこちらのEC2インスタンスのAMIを作成してバックアップを取得するようにします。AMIの作成はコンソール画面からAMIの作成がありますので、そちらからでも可能ですが今回はコマンドベースで作成していきます。
AWS CLIを使ってAMIの作成を行いますが、コマンドはこの一文を打ち込むだけです。
aws ec2 create-image --instance-id <構築したインスタンスのID> --name "任意のAMI名" --reboot
こちらを実行することで簡単にAMIを作成することができます。
反省点
以前読んだAnsibleの入門本を使ってAmazon Linux2へWordPressを導入するのはそんなに難しくないかと思いますが、レポジトリ周りで色々とエラーがでて思ったより苦戦しました。また本来ならAMIの作成をAWS CLIではなくPackerというツールを使って作成しようとしましたが、どうやらはこれは既存のEC2インスタンスに対してはAMIの作成ができないみたいで、Packerのテンプレート文でAMIの作成時の状態を設定する必要があるようです。
次回はAnsibleとPackerを組み合わせて、EC2インスタンスを立てることなくWordPressが入ったAmazon Linux2のAMIを作成しようと思います。