データアナリティクス事業本部の鈴木です。
EC2を対象に、Ansibleでサーバー構築をする機会がありました。既存の設定を使うだけではなく、自分でもいろいろ動かして勉強してみたかったので、練習環境を構築するためのCloudFormationテンプレートと、簡単なプレイブックを作成したのでご紹介します。
やりたかったこと
この記事では、以下を試してみました。
- Ansibleで環境構築を行うためのEC2インスタンスおよび必要なリソース(VPCやエンドポイントをはじめとしたネットワークリソースなど)を作成する。
- AnsibleでEC2インスタンスにソフトウェアをインストールする。
Ansibleはローカル環境を対象にもできますが、この記事を見ているような方だと、AWS環境上にあるEC2を対象とした場合があるように思います。
その場合、練習に使える出来立てのネットワークとEC2インスタンスがあるといいのですが、そうでない場合は1から作ることになるのでやりたいことに対して多少の手間が発生します。そのため、Ansibleのサンプルの実行に加えて、AWS上の環境構築についても行いました。
前提
Ansibleのインストール
今回はpipにてインストールしました。
pip install ansible
ansible --version
# ansible [core 2.14.5]
# (略)
構築する環境
以下のような構成を構築します。
ポイントとしては以下です。
- NATゲートウェイ経由でインターネットに出られるようにしています。Ansibleでソフトウェアをインストールする際に必要になります。
- Session Managerで接続できるように、必要なエンドポイントとセキュリティグループを作成しています。
- ゲートウェイ型のS3エンドポイントを作成しています。
- EC2インスタンスはAmazon Linux 2023を使用しています。
デプロイしてEC2が起動すれば、Session Managerを使って接続できるようにします。
やってみた
1. 構成のデプロイ
CloudFormationから以下のテンプレートをデプロイしました。
EC2のキーペアは先に作成してある想定です。
AWSTemplateFormatVersion: "2010-09-09"
Parameters:
EnvironmentName:
Type: String
VPCCIDR:
Type: String
Default: 10.192.0.0/16
PublicSubnetCIDR:
Type: String
Default: 10.192.1.0/24
PrivateSubnetCIDR:
Type: String
Default: 10.192.0.0/24
Ec2ImageId:
Type: AWS::SSM::Parameter::Value<String>
Default: /aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64
Ec2InstanceType:
Type: String
Default: t3.micro
KeyPair:
Type: String
Default: xxxxx-key
Resources:
VPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: !Ref VPCCIDR
EnableDnsSupport: true
EnableDnsHostnames: true
Tags:
- Key: Name
Value: !Sub ${EnvironmentName}-VPC
# InternetGateway
InternetGateway:
Type: AWS::EC2::InternetGateway
Properties:
Tags:
- Key: Name
Value: !Sub ${EnvironmentName}-igw
AttachGateway:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
VpcId: !Ref VPC
InternetGatewayId: !Ref InternetGateway
NatGateway:
Type: AWS::EC2::NatGateway
Properties:
AllocationId: !GetAtt NatGatewayEIP.AllocationId
SubnetId: !Ref PublicSubnet
Tags:
- Key: Name
Value: !Sub ${EnvironmentName}-ngw
NatGatewayEIP:
Type: AWS::EC2::EIP
Properties:
Domain: vpc
# Public Subnetのネットワーク設定
PublicSubnet:
Type: AWS::EC2::Subnet
Properties:
AvailabilityZone: !Select [0, !GetAZs ""]
CidrBlock: !Ref PublicSubnetCIDR
VpcId: !Ref VPC
Tags:
- Key: Name
Value: !Sub ${EnvironmentName}-PublicSubnet
PublicRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
Tags:
- Key: Name
Value: !Sub ${EnvironmentName}-PublicRouteTable
PublicRoute:
Type: AWS::EC2::Route
Properties:
RouteTableId: !Ref PublicRouteTable
DestinationCidrBlock: 0.0.0.0/0
GatewayId: !Ref InternetGateway
PublicSubnetRouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PublicSubnet
RouteTableId: !Ref PublicRouteTable
# Private Subnetのネットワーク設定
PrivateSubnet:
Type: AWS::EC2::Subnet
Properties:
AvailabilityZone: !Select [0, !GetAZs ""]
CidrBlock: !Ref PrivateSubnetCIDR
VpcId: !Ref VPC
Tags:
- Key: Name
Value: !Sub ${EnvironmentName}-PrivateSubnet
PrivateRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
Tags:
- Key: Name
Value: !Sub ${EnvironmentName}-PrivateRouteTable
PrivateRoute:
Type: AWS::EC2::Route
Properties:
RouteTableId: !Ref PrivateRouteTable
DestinationCidrBlock: 0.0.0.0/0
NatGatewayId: !Ref NatGateway
PrivateSubnetRouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PrivateSubnet
RouteTableId: !Ref PrivateRouteTable
# エンドポイントの設定
EndpointSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: EndpointSecurityGroup
VpcId: !Ref VPC
Tags:
- Key: Name
Value: !Sub ${EnvironmentName}-EndpointSecurityGroup
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 443
ToPort: 443
CidrIp: !Ref VPCCIDR
EndpointSSM:
Type: AWS::EC2::VPCEndpoint
Properties:
PrivateDnsEnabled: true
SecurityGroupIds:
- !Ref EndpointSecurityGroup
ServiceName: !Sub com.amazonaws.${AWS::Region}.ssm
SubnetIds:
- !Ref PrivateSubnet
VpcEndpointType: Interface
VpcId: !Ref VPC
EndpointSSMMessages:
Type: AWS::EC2::VPCEndpoint
Properties:
PrivateDnsEnabled: true
SecurityGroupIds:
- !Ref EndpointSecurityGroup
ServiceName: !Sub com.amazonaws.${AWS::Region}.ssmmessages
SubnetIds:
- !Ref PrivateSubnet
VpcEndpointType: Interface
VpcId: !Ref VPC
EndpointEC2Messages:
Type: AWS::EC2::VPCEndpoint
Properties:
PrivateDnsEnabled: true
SecurityGroupIds:
- !Ref EndpointSecurityGroup
ServiceName: !Sub com.amazonaws.${AWS::Region}.ec2messages
SubnetIds:
- !Ref PrivateSubnet
VpcEndpointType: Interface
VpcId: !Ref VPC
EndpointS3:
Type: AWS::EC2::VPCEndpoint
Properties:
RouteTableIds:
- !Ref PrivateRouteTable
ServiceName: !Sub com.amazonaws.${AWS::Region}.s3
VpcEndpointType: Gateway
VpcId: !Ref VPC
EC2IAMRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub ${EnvironmentName}-SSM-role
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
Service:
- ec2.amazonaws.com
Action:
- sts:AssumeRole
Path: /
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
EC2InstanceProfile:
Type: AWS::IAM::InstanceProfile
Properties:
Path: /
Roles:
- Ref: EC2IAMRole
InstanceProfileName: !Sub ${EnvironmentName}-EC2InstanceProfile
EC2SecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: EC2SecurityGroup
VpcId: !Ref VPC
Tags:
- Key: Name
Value: !Sub ${EnvironmentName}-EC2SecurityGroup
EC2Instance:
Type: AWS::EC2::Instance
Properties:
InstanceType: !Ref Ec2InstanceType
SubnetId: !Ref PrivateSubnet
ImageId: !Ref Ec2ImageId
SecurityGroupIds:
- !Ref EC2SecurityGroup
IamInstanceProfile: !Ref EC2InstanceProfile
BlockDeviceMappings:
- DeviceName: /dev/xvda
Ebs:
VolumeSize: 50
VolumeType: gp3
EbsOptimized: true
SourceDestCheck: true
KeyName: !Ref KeyPair
Tags:
- Key: Name
Value: !Sub ${EnvironmentName}-EC2Instance
Outputs:
VPC:
Value: !Ref VPC
Export:
Name: !Sub ${EnvironmentName}-VPC
VPCCIDR:
Value: !Ref VPCCIDR
Export:
Name: !Sub ${EnvironmentName}-VPCCIDR
PrivateSubnet:
Value: !Ref PrivateSubnet
Export:
Name: !Sub ${EnvironmentName}-PrivateSubnet
PrivateRouteTable:
Value: !Ref PrivateRouteTable
Export:
Name: !Sub ${EnvironmentName}-PrivateRouteTable
SecurityGroup:
Value: !Ref EndpointSecurityGroup
Export:
Name: !Sub ${EnvironmentName}-EndpointSecurityGroup
EndpointSSM:
Value: !Ref EndpointSSM
Export:
Name: !Sub ${EnvironmentName}-EndpointSSM
EndpointSSMMessages:
Value: !Ref EndpointSSMMessages
Export:
Name: !Sub ${EnvironmentName}-EndpointSSMMessages
EndpointEC2Messages:
Value: !Ref EndpointEC2Messages
Export:
Name: !Sub ${EnvironmentName}-EndpointEC2Messages
EndpointS3:
Value: !Ref EndpointS3
Export:
Name: !Sub ${EnvironmentName}-EndpointS3
EC2SecurityGroup:
Value: !Ref EC2SecurityGroup
Export:
Name: !Sub ${EnvironmentName}-EC2SecurityGroup
EC2Instance:
Value: !Ref EC2Instance
Export:
Name: !Sub ${EnvironmentName}-EC2Instance
テンプレートは以下のブログを参考にしました。
CloudFormationでスタックがCREATE_COMPLETE
になることを確認しました。
2. EC2インスタンスへのSSH接続確認
今回デプロイしたEC2インスタンスはセッションマネージャー経由でSSH接続できるので、本当に接続できるか確認しました。
.ssh/config
に以下を追記しました。ただし、インスタンスID
・プロファイル名
・秘密鍵のパス
は作成したインスタンスやローカル環境に設定しているものに置き換えました。
host cm-nayuts-ansible-sample
ProxyCommand sh -c "aws ssm start-session --target <インスタンスID> --document-name AWS-StartSSHSession --parameters 'portNumber=%p' --profile <プロファイル名> --region ap-northeast-1"
User ec2-user
IdentityFile <秘密鍵のパス>
sshコマンドで接続すると、ログインできました。
ssh cm-nayuts-ansible-sample
# , #_
# ~\_ ####_ Amazon Linux 2023
# ~~ \_#####\
# ~~ \###|
# ~~ \#/ ___ https://aws.amazon.com/linux/amazon-linux-2023
# ~~ V~' '->
# ~~~ /
# ~~._. _/
# _/ _/
# _/m/'
# Last login: Sat May 20 05:52:40 2023 from 127.0.0.1
[ec2-user@ip-<ローカルのアドレス> ~]$
3. Ansible用のファイルの作成
ansible
ディレクトリを作成し、以下のようにファイルを作成しました。
tree ansible
# .
# ├── ansible.cfg
# ├── hosts
# ├── playbook.yml
# └── ssh_config
ファイル名などは以下のブログを参考にしました。
ansible.cfgファイルの作成
ansible.cfg
は以下のようにしました。
ansible.cfg
[defaults]
inventory = hosts
[privilege_escalation]
become = True
[ssh_connection]
control_path = %(directory)s/%%h-%%r
ssh_args = -o ControlPersist=15m -F ssh_config -q
scp_if_ssh = True
特に、ansible.cfg
の内容はAnsible Configuration Settingsで最新の仕様を確認しました。
ssh_args
はman ssh
でsshコマンドのオプションの内容を確認しました。特に、-F ssh_config
でSSHの設定をファイルに外出ししました。
ssh_configファイルの作成
SSH接続の情報を記載するファイルです。
先にSSH接続可否を確認した際に、.ssh/config
に追記した内容と同じものを記載しました。
.ssh/config
host cm-nayuts-ansible-sample
ProxyCommand sh -c "aws ssm start-session --target <インスタンスID> --document-name AWS-StartSSHSession --parameters 'portNumber=%p' --profile <プロファイル名> --region ap-northeast-1"
User ec2-user
IdentityFile <SSH鍵のパス>
hostsファイルの作成
ansibleのインベントリです。
.iniファイルの形式で以下のように記載しました。
hosts
[cm-nayuts-sample]
cm-nayuts-ansible-sample
インベントリの書き方はドキュメントの以下のページが参考になりました。
playbook.ymlファイルの作成
自動化する内容を記載するファイルです。
playbook.yml
- hosts: cm-nayuts-sample
tasks:
- name: Ensure jq is at the latest version
ansible.builtin.dnf:
name: jq
state: latest
プレイブックの書き方として、ドキュメントの以下のページを参考にしました。
また、Taskで使用したansible.builtin.dnf
モジュールを使ってjq
をインストールしてみました。
記載する設定は以下のドキュメントを参考に書いてみました。
今回はAmazon Linux 2023で試していますが、Amazon Linux 2023がGAされましたでパッケージマネージャーがdnf
に変更されていることと、Amazon Linux 2023 packages updated 2023-05-01にjqがあることを確認したので、そのような設定としました。
なお、インスタンスにjq
がインストールされているか確認しましたが、あらかじめインストールされていませんでした。プレイブックの実行後にインストールされていれば、正常にAnsibleからインストールできたことが分かりやすいですね。
[ec2-user@ip-<ローカルのアドレス> ~]$ which jq
# /usr/bin/which: no jq in (/home/ec2-user/.local/bin:/home/ec2-user/bin:/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin)
4. プレイブックの実行
最後に実際にプレイブックを実行してみました。
ローカルで以下のコマンドを実行しました。
[ec2-user@ip-<ローカルのアドレス> ~]$ ansible-playbook ./playbook.yml
# [WARNING]: Invalid characters were found in group names but not replaced, use -vvvv to see details
#
# PLAY [cm-nayuts-sample] ************************************************************************************************
#
# TASK [Gathering Facts] *************************************************************************************************
# [WARNING]: Platform linux on host cm-nayuts-ansible-sample is using the discovered Python interpreter at
# /usr/bin/python3.9, but future installation of another Python interpreter could change the meaning of that path. See
# https://docs.ansible.com/ansible-core/2.14/reference_appendices/interpreter_discovery.html for more information.
# ok: [cm-nayuts-ansible-sample]
#
# TASK [Ensure jq is at the latest version] ******************************************************************************
# changed: [cm-nayuts-ansible-sample]
# PLAY RECAP *************************************************************************************************************
# cm-nayuts-ansible-sample : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
jq
のインストールが無事に完了しました。
Pythonインタプリタに関する警告が出ていますが、対処方法は以下のブログに紹介がありました。今回は特に気にしないこととします。
SSHログインしてみると、確かにjq
がインストールされていることを確認できました。
[ec2-user@ip-<ローカルのアドレス> ~]$ which jq
# /usr/bin/jq
5. 片付け
作成したCloudFormationスタックを削除することで、検証用のリソースは削除できます。特にNATゲートウェイなど処理データ量に加えて時間単位でも課金が発生するものがあるため、検証目的の場合は終わったら削除しておきましょう。
最後に
今回はAnsibleを使って、Amazon Linux 2023にソフトウェアをインストールする例を紹介しました。
デプロイするだけでセッションマネージャーですぐにログインできるEC2インスタンスをVPCからまとめて作成できるCloudFormationテンプレートを作成したので、いつでもAnsibleの練習ができますね!
参考になりましたら幸いです。