EC2でAnsible練習環境を作成してみた

2023.05.22

データアナリティクス事業本部の鈴木です。

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_argsman 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の練習ができますね!

参考になりましたら幸いです。