想ひ出のへっぽこBlogⅡ from35

~ 自身の備忘録および学習促進のためにブログります。~

想ひ出24: AWS/CloudFormationに入門する/2

f:id:moqrin3:20190114193742p:plain

おはようございます、moqrinです。

前回の概要に続き、今回はCloudFormationの基本的な対応のまとめです。

スタックの作成、更新という基本操作を行っていきます。

内容としては、新規VPCを作成して、パブリックサブネットとプライベートサブネットを各2つずつ、 ALBを作成してパブリックサブネットを紐付けて、RDSを作って、Auto Scaling グループ内に Amazon EC2 インスタンスを作成します。

AWS CloudFormation Designerから図示したものがこちらです。

f:id:moqrin3:20190126153038p:plain

中身としては、サンプルソリューションの「スケーラブルで耐久性の高い WordPress」に VPCとPublic,Privateのサブネットを新規追加したものですね。

目次

1. テンプレートでスタックを新規作成

2. スタックを変更セットを使って更新

3. 宿題


1. テンプレートでスタックを新規作成

下記のテンプレートからスタックを作成します。

# VPC_Multi_RDS_AutoScaling_EC2_instance.json
{
  "AWSTemplateFormatVersion": "2010-09-09",
  "Description": "AWS CloudFormation VPC_RDS_ALB_AutoScaling_EC2Instances",

  "Parameters" : {

  "KeyName": {
    "Description": "Name of an existing EC2 KeyPair to enable SSH access to the instances",
    "Type": "AWS::EC2::KeyPair::KeyName",
    "ConstraintDescription": "must be the name of an existing EC2 KeyPair."
  },
  "DBName": {
    "Default": "myDatabase",
    "Description": "MySQL database name",
    "Type": "String",
    "MinLength": "8",
    "MaxLength": "64",
    "AllowedPattern": "[a-zA-Z][a-zA-Z0-9]*",
    "ConstraintDescription": "must begin with a letter and contain only alphanumeric characters."
  },
  "DBUser": {
    "NoEcho": "true",
    "Default": "root",
    "Description": "Username for MySQL database access",
    "Type": "String",
    "MinLength": "3",
    "MaxLength": "16",
    "AllowedPattern": "[a-zA-Z][a-zA-Z0-9]*",
    "ConstraintDescription": "must begin with a letter and contain only alphanumeric characters."
  },
  "DBPassword": {
    "NoEcho": "true",
    "Default": "samplePasswd",
    "Description": "Password for MySQL database access",
    "Type": "String",
    "MinLength": "8",
    "MaxLength": "41",
    "AllowedPattern": "[a-zA-Z0-9]*",
    "ConstraintDescription": "must contain only alphanumeric characters."
  },
  "DBAllocatedStorage": {
    "Default": "5",
    "Description": "The size of the database (Gb)",
    "Type": "Number",
    "MinValue": "5",
    "MaxValue": "1024",
    "ConstraintDescription": "must be between 5 and 1024Gb."
  },
  "DBInstanceClass": {
    "Description": "The database instance type",
    "Type": "String",
    "Default": "db.t2.micro",
    "AllowedValues": [
      "db.t1.micro",
      "db.t2.micro"
    ],
    "ConstraintDescription": "must select a valid database instance type."
  },
  "MultiAZDatabase": {
    "Default": "false",
    "Description": "Create a Multi-AZ MySQL Amazon RDS database instance",
    "Type": "String",
    "AllowedValues": [
      "true",
      "false"
    ],
    "ConstraintDescription": "must be either true or false."
  },
  "WebServerCapacity": {
    "Default": "1",
    "Description": "The initial number of WebServer instances",
    "Type": "Number",
    "MinValue": "1",
    "MaxValue": "3",
    "ConstraintDescription": "must be between 1 and 3 EC2 instances."
  },
  "InstanceType": {
    "Description": "WebServer EC2 instance type",
    "Type": "String",
    "Default": "t2.micro",
    "AllowedValues": [
      "t1.micro",
      "t2.nano",
      "t2.micro"
    ],
    "ConstraintDescription": "must be a valid EC2 instance type."
  },
  "SSHLocation": {
    "Description": " The IP address range that can be used to SSH to the EC2 instances",
    "Type": "String",
    "MinLength": "9",
    "MaxLength": "18",
    "Default": "0.0.0.0/0",
    "AllowedPattern": "(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})/(\\d{1,2})",
    "ConstraintDescription": "must be a valid IP CIDR range of the form x.x.x.x/x."
  }
},

  "Mappings" : {
    "AWSInstanceType2Arch" : {
      "t1.micro"    : { "Arch" : "HVM64"  },
      "t2.nano"     : { "Arch" : "HVM64"  },
      "t2.micro"    : { "Arch" : "HVM64"  }
    },

    "AWSRegionArch2AMI" : {
      "us-east-1"        : {"HVM64" : "ami-0ff8a91507f77f867", "HVMG2" : "ami-0a584ac55a7631c0c"},
      "us-west-2"        : {"HVM64" : "ami-a0cfeed8", "HVMG2" : "ami-0e09505bc235aa82d"},
      "us-west-1"        : {"HVM64" : "ami-0bdb828fd58c52235", "HVMG2" : "ami-066ee5fd4a9ef77f1"},
      "ap-northeast-1"   : {"HVM64" : "ami-00a5245b4816c38e6", "HVMG2" : "ami-053cdd503598e4a9d"}
    }

  },

  "Resources" : {
    "VPC" : {
      "Type" : "AWS::EC2::VPC",
      "Properties" : {
        "CidrBlock" : "10.0.0.0/16",
        "Tags" : [ {"Key" : "Name", "Value" : "Sample-VPC" },{"Key" : "Application", "Value" : { "Ref" : "AWS::StackId"} } ]
      }
    },

    "PublicSubnet1" : {
      "Type" : "AWS::EC2::Subnet",
      "Properties" : {
        "VpcId" : { "Ref" : "VPC" },
        "CidrBlock" : "10.0.0.0/24",
        "AvailabilityZone" : { "Fn::Select" : [ "0", { "Fn::GetAZs" : { "Ref" : "AWS::Region" } }]},
        "Tags" : [ {"Key" : "Name", "Value" : "PublicSubnet1" },{"Key" : "Application", "Value" : { "Ref" : "AWS::StackId"} } ]
      }
    },

    "PublicSubnet2" : {
      "Type" : "AWS::EC2::Subnet",
      "Properties" : {
        "VpcId" : { "Ref" : "VPC" },
        "CidrBlock" : "10.0.1.0/24",
        "AvailabilityZone" : { "Fn::Select" : [ "1", { "Fn::GetAZs" : { "Ref" : "AWS::Region" } }]},
        "Tags" : [ {"Key" : "Name", "Value" : "PublicSubnet2" },{"Key" : "Application", "Value" : { "Ref" : "AWS::StackId"} } ]
      }
    },

    "PrivateSubnet1" : {
      "Type" : "AWS::EC2::Subnet",
      "Properties" : {
        "VpcId" : { "Ref" : "VPC" },
        "CidrBlock" : "10.0.2.0/24",
        "AvailabilityZone" : { "Fn::Select" : [ "0", { "Fn::GetAZs" : { "Ref" : "AWS::Region" } }]},
        "Tags" : [ {"Key" : "Name", "Value" : "PrivateSubnet1" },{"Key" : "Application", "Value" : { "Ref" : "AWS::StackId"} } ]
      }
    },

    "PrivateSubnet2" : {
      "Type" : "AWS::EC2::Subnet",
      "Properties" : {
        "VpcId" : { "Ref" : "VPC" },
        "CidrBlock" : "10.0.3.0/24",
        "AvailabilityZone" : { "Fn::Select" : [ "1", { "Fn::GetAZs" : { "Ref" : "AWS::Region" } }]},
        "Tags" : [ {"Key" : "Name", "Value" : "PrivateSubnet1" },{"Key" : "Application", "Value" : { "Ref" : "AWS::StackId"} } ]
      }
    },

    "InternetGateway" : {
      "Type" : "AWS::EC2::InternetGateway",
      "Properties" : {
        "Tags" : [ {"Key" : "Application", "Value" : { "Ref" : "AWS::StackId"} } ]
      }
    },

    "AttachGateway" : {
      "Type" : "AWS::EC2::VPCGatewayAttachment",
      "Properties" : {
        "VpcId" : { "Ref" : "VPC" },
        "InternetGatewayId" : { "Ref" : "InternetGateway" }
      }
    },

    "PublicRouteTable" : {
      "Type" : "AWS::EC2::RouteTable",
      "Properties" : {
        "VpcId" : {"Ref" : "VPC"},
        "Tags" : [ {"Key" : "Application", "Value" : { "Ref" : "AWS::StackId"} } ]
      }
    },

    "PublicRoute" : {
      "Type" : "AWS::EC2::Route",
      "DependsOn" : "AttachGateway",
      "Properties" : {
        "RouteTableId" : { "Ref" : "PublicRouteTable" },
        "DestinationCidrBlock" : "0.0.0.0/0",
        "GatewayId" : { "Ref" : "InternetGateway" }
      }
    },

    "Public1SubnetRouteTableAssociation" : {
      "Type" : "AWS::EC2::SubnetRouteTableAssociation",
      "Properties" : {
        "SubnetId" : { "Ref" : "PublicSubnet1" },
        "RouteTableId" : { "Ref" : "PublicRouteTable" }
      }
    },

    "Public2SubnetRouteTableAssociation" : {
      "Type" : "AWS::EC2::SubnetRouteTableAssociation",
      "Properties" : {
        "SubnetId" : { "Ref" : "PublicSubnet2" },
        "RouteTableId" : { "Ref" : "PublicRouteTable" }
      }
    },

    "NetworkAcl" : {
      "Type" : "AWS::EC2::NetworkAcl",
      "Properties" : {
        "VpcId" : {"Ref" : "VPC"},
        "Tags" : [ {"Key" : "Application", "Value" : { "Ref" : "AWS::StackId"} } ]
      }
    },

    "InboundPublicNetworkAclEntry" : {
      "Type" : "AWS::EC2::NetworkAclEntry",
      "Properties" : {
        "CidrBlock" : "0.0.0.0/0",
        "Egress" : "false",
        "NetworkAclId" : { "Ref" : "NetworkAcl" },
        "Protocol" : "-1",
        "RuleAction" : "allow",
        "RuleNumber" : "100"
      }
    },

    "OutboundPublicNetworkAclEntry" : {
      "Type" : "AWS::EC2::NetworkAclEntry",
      "Properties" : {
        "CidrBlock" : "0.0.0.0/0",
        "Egress" : "true",
        "NetworkAclId" : { "Ref" : "NetworkAcl" },
        "Protocol" : "-1",
        "RuleAction" : "allow",
        "RuleNumber" : "100"
      }
    },

    "PublicSubnet1NetworkAclAssociation" : {
      "Type" : "AWS::EC2::SubnetNetworkAclAssociation",
      "Properties" : {
        "SubnetId" : { "Ref" : "PublicSubnet1" },
        "NetworkAclId" : { "Ref" : "NetworkAcl" }
      }
    },

    "PublicSubnet2NetworkAclAssociation" : {
      "Type" : "AWS::EC2::SubnetNetworkAclAssociation",
      "Properties" : {
        "SubnetId" : { "Ref" : "PublicSubnet2" },
        "NetworkAclId" : { "Ref" : "NetworkAcl" }
      }
    },

    "ApplicationLoadBalancer" : {
      "Type" : "AWS::ElasticLoadBalancingV2::LoadBalancer",
      "Properties" : {
        "SecurityGroups" : [{"Ref" : "ALBSecurityGroup"}],
        "Subnets" : [{"Ref": "PublicSubnet1"},{ "Ref" : "PublicSubnet2" }],
        "Tags" : [ {"Key" : "Name", "Value" : "Sample-ALB" } ]
      }
    },

    "ALBListener" : {
      "Type" : "AWS::ElasticLoadBalancingV2::Listener",
      "Properties" : {
        "DefaultActions" : [{
          "Type" : "forward",
          "TargetGroupArn" : { "Ref" : "ALBTargetGroup" }
        }],
        "LoadBalancerArn" : { "Ref" : "ApplicationLoadBalancer" },
        "Port" : "80",
        "Protocol" : "HTTP"
      }
    },

    "ALBTargetGroup" : {
      "Type" : "AWS::ElasticLoadBalancingV2::TargetGroup",
      "Properties" : {
        "HealthCheckIntervalSeconds" : 10,
        "HealthCheckTimeoutSeconds" : 5,
        "HealthyThresholdCount" : 2,
        "Port" : 80,
        "Protocol" : "HTTP",
        "UnhealthyThresholdCount" : 5,
        "VpcId" : {"Ref" : "VPC"}
      }
    },

    "WebServerGroup" : {
      "Type" : "AWS::AutoScaling::AutoScalingGroup",
      "Properties" : {
        "VPCZoneIdentifier" : [{"Ref": "PublicSubnet1"},{ "Ref" : "PublicSubnet2" }],
        "LaunchConfigurationName" : { "Ref" : "LaunchConfig" },
        "MinSize" : "1",
        "MaxSize" : "3",
        "DesiredCapacity" : { "Ref" : "WebServerCapacity" },
        "TargetGroupARNs" : [ { "Ref" : "ALBTargetGroup" } ]
      },

      "UpdatePolicy": {
        "AutoScalingRollingUpdate": {
          "MinInstancesInService": "1",
          "MaxBatchSize": "1",
          "PauseTime" : "PT15M",
          "WaitOnResourceSignals": "true"
        }
      }
    },

    "DBSubnetGroup" : {
      "Type" : "AWS::RDS::DBSubnetGroup",
      "Properties" : {
        "DBSubnetGroupDescription" : "This is a privateSubnet",
        "SubnetIds": [{"Ref": "PrivateSubnet1"},{ "Ref" : "PrivateSubnet2" }]
      }
    },

    "LaunchConfig": {
      "Type" : "AWS::AutoScaling::LaunchConfiguration",
      "Properties": {
        "AssociatePublicIpAddress": true,
        "ImageId" : { "Fn::FindInMap" : [ "AWSRegionArch2AMI", { "Ref" : "AWS::Region" },
          { "Fn::FindInMap" : [ "AWSInstanceType2Arch", { "Ref" : "InstanceType" }, "Arch" ] } ] },
        "InstanceType" : { "Ref" : "InstanceType" },
        "SecurityGroups" : [ {"Ref" : "WebServerSecurityGroup"} ],
        "KeyName" : { "Ref" : "KeyName" }
      }
    },

    "WebServerSecurityGroup" : {
      "Type" : "AWS::EC2::SecurityGroup",
      "Properties" : {
        "GroupDescription" : "Enable HTTP access via port 80 locked down to the ELB and SSH access",
        "SecurityGroupIngress" : [
          { "IpProtocol" : "tcp", "FromPort" : "80", "ToPort" : "80", "CidrIp" : "0.0.0.0/0"},
          {"IpProtocol" : "tcp", "FromPort" : "22", "ToPort" : "22", "CidrIp" : { "Ref" : "SSHLocation"}
          } ],
        "VpcId" : { "Ref" : "VPC" }
      }
    },

    "ALBSecurityGroup" : {
      "Type" : "AWS::EC2::SecurityGroup",
      "Properties" : {
        "GroupName": "http",
        "GroupDescription" : "Enable HTTP access via port 80",
        "SecurityGroupIngress" : [
          { "IpProtocol" : "tcp", "FromPort" : "80", "ToPort" : "80", "CidrIp" : "0.0.0.0/0"}],
        "VpcId" : { "Ref" : "VPC" }
      }
    },

    "DBEC2SecurityGroup": {
      "Type": "AWS::EC2::SecurityGroup",
      "Properties" : {
        "GroupDescription": "Open database for access",
        "SecurityGroupIngress" : [{
          "IpProtocol" : "tcp",
          "FromPort" : "3306",
          "ToPort" : "3306",
          "SourceSecurityGroupId" : { "Ref" : "WebServerSecurityGroup" }
        }],
        "VpcId" : { "Ref" : "VPC" }
      }
    },

    "MySQLDatabase": {
      "Type": "AWS::RDS::DBInstance",
      "Properties": {
        "Engine" : "MySQL",
        "DBName" : { "Ref": "DBName" },
        "MultiAZ" : { "Ref": "MultiAZDatabase" },
        "MasterUsername": { "Ref": "DBUser" },
        "MasterUserPassword": { "Ref" : "DBPassword" },
        "DBInstanceClass": { "Ref" : "DBInstanceClass" },
        "AllocatedStorage": { "Ref" : "DBAllocatedStorage" },
        "DBSubnetGroupName": { "Ref": "DBSubnetGroup" },
        "VPCSecurityGroups": [ { "Fn::GetAtt": [ "DBEC2SecurityGroup", "GroupId" ] } ]
      }
    }
  },

  "Outputs" : {
    "WebsiteURL" : {
      "Description" : "URL for newly created LAMP stack",
      "Value" : { "Fn::Join" : ["", ["http://", { "Fn::GetAtt" : [ "ApplicationLoadBalancer", "DNSName" ]}]] }
    }
  }
}

ローカルにあるテンプレート内容に問題がないか確認しておきます。

# テンプレートの検証
aws cloudformation validate-template --template-body file://`pwd`/VPC_Multi_RDS_AutoScaling_EC2_instance.json

問題なければスタックを作成します。 (パラメータ指定については、インスタンスタイプ、DB名、DBユーザー、DBパスワードなど省略してしまっていますー。)

# スタックの新規作成
aws cloudformation create-stack --stack-name sample-stack --template-body file://`pwd`/VPC_Multi_RDS_AutoScaling_EC2_instance.json \
--parameters ParameterKey=KeyName,ParameterValue=YOUR_KEY_NAME\
ParameterKey=SSHLocation,ParameterValue=xxx.xxx.xxx.xxx/32

できました。

f:id:moqrin3:20190126153137p:plain

アプリケーションを何も入れていないので、ALBの方ではunhealtyになっています。

f:id:moqrin3:20190126153158p:plain

なので、Wordpressでもインストールしてみますか。

2. スタックを変更セットを使って更新

変更セットを使用すると、スタックの変更案が実行中のリソースに与える可能性がある影響 (たとえば、変更によって重要なリソースが削除されたり置き換えられたりしないか) を確認できたり、変更セットの実行を確定したときに限り、スタックが変更されるため、変更案のまま続行するか別の変更セットを作成して他の変更を検討するかを決定できるようです。

まずは、既存のテンプレート内容を取得します。

# スタックを取得
aws cloudformation get-template --stack-name sample-stack > VPC_Multi_RDS_AutoScaling_EC2_instance_wp.json

元々のサンプルから"LaunchConfig"の部分をそのまま上書いて、諸々修正したのが下記テンプレートです。(冗長)

# スタックを取得して加筆修正
{
  "Description": "AWS CloudFormation VPC_RDS_ALB_AutoScaling_EC2Instances",
  "Parameters": {
    "MultiAZDatabase": {
      "Default": "false",
      "ConstraintDescription": "must be either true or false.",
      "Type": "String",
      "Description": "Create a Multi-AZ MySQL Amazon RDS database instance",
      "AllowedValues": [
        "true",
        "false"
      ]
    },
    "DBAllocatedStorage": {
      "Description": "The size of the database (Gb)",
      "Default": "5",
      "Type": "Number",
      "MaxValue": "1024",
      "MinValue": "5",
      "ConstraintDescription": "must be between 5 and 1024Gb."
    },
    "InstanceType": {
      "Default": "t2.micro",
      "ConstraintDescription": "must be a valid EC2 instance type.",
      "Type": "String",
      "Description": "WebServer EC2 instance type",
      "AllowedValues": [
        "t1.micro",
        "t2.nano",
        "t2.micro"
      ]
    },
    "SSHLocation": {
      "ConstraintDescription": "must be a valid IP CIDR range of the form x.x.x.x/x.",
      "Description": " The IP address range that can be used to SSH to the EC2 instances",
      "Default": "0.0.0.0/0",
      "MinLength": "9",
      "AllowedPattern": "(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})/(\\d{1,2})",
      "MaxLength": "18",
      "Type": "String"
    },
    "KeyName": {
      "ConstraintDescription": "must be the name of an existing EC2 KeyPair.",
      "Type": "AWS::EC2::KeyPair::KeyName",
      "Description": "Name of an existing EC2 KeyPair to enable SSH access to the instances"
    },
    "DBPassword": {
      "ConstraintDescription": "must contain only alphanumeric characters.",
      "Description": "Password for MySQL database access",
      "Default": "samplePasswd",
      "MinLength": "8",
      "AllowedPattern": "[a-zA-Z0-9]*",
      "NoEcho": "true",
      "MaxLength": "41",
      "Type": "String"
    },
    "DBUser": {
      "ConstraintDescription": "must begin with a letter and contain only alphanumeric characters.",
      "Description": "Username for MySQL database access",
      "Default": "root",
      "MinLength": "4",
      "AllowedPattern": "[a-zA-Z][a-zA-Z0-9]*",
      "NoEcho": "true",
      "MaxLength": "16",
      "Type": "String"
    },
    "DBInstanceClass": {
      "Default": "db.t2.micro",
      "ConstraintDescription": "must select a valid database instance type.",
      "Type": "String",
      "Description": "The database instance type",
      "AllowedValues": [
        "db.t1.micro",
        "db.t2.micro"
      ]
    },
    "DBName": {
      "ConstraintDescription": "must begin with a letter and contain only alphanumeric characters.",
      "Description": "MySQL database name",
      "Default": "myDatabase",
      "MinLength": "4",
      "AllowedPattern": "[a-zA-Z][a-zA-Z0-9]*",
      "MaxLength": "64",
      "Type": "String"
    },
    "WebServerCapacity": {
      "Description": "The initial number of WebServer instances",
      "Default": "1",
      "Type": "Number",
      "MaxValue": "3",
      "MinValue": "1",
      "ConstraintDescription": "must be between 1 and 3 EC2 instances."
    }
  },
  "AWSTemplateFormatVersion": "2010-09-09",
  "Outputs": {
    "WebsiteURL": {
      "Description": "URL for newly created LAMP stack",
      "Value": {
        "Fn::Join": [
          "",
          [
            "http://",
            {
              "Fn::GetAtt": [
                "ApplicationLoadBalancer",
                "DNSName"
              ]
            }
          ]
        ]
      }
    }
  },
  "Resources": {
    "NetworkAcl": {
      "Type": "AWS::EC2::NetworkAcl",
      "Properties": {
        "VpcId": {
          "Ref": "VPC"
        },
        "Tags": [
          {
            "Value": {
              "Ref": "AWS::StackId"
            },
            "Key": "Application"
          }
        ]
      }
    },
    "PublicRoute": {
      "Type": "AWS::EC2::Route",
      "Properties": {
        "GatewayId": {
          "Ref": "InternetGateway"
        },
        "DestinationCidrBlock": "0.0.0.0/0",
        "RouteTableId": {
          "Ref": "PublicRouteTable"
        }
      },
      "DependsOn": "AttachGateway"
    },
    "PublicSubnet1NetworkAclAssociation": {
      "Type": "AWS::EC2::SubnetNetworkAclAssociation",
      "Properties": {
        "SubnetId": {
          "Ref": "PublicSubnet1"
        },
        "NetworkAclId": {
          "Ref": "NetworkAcl"
        }
      }
    },
    "InternetGateway": {
      "Type": "AWS::EC2::InternetGateway",
      "Properties": {
        "Tags": [
          {
            "Value": {
              "Ref": "AWS::StackId"
            },
            "Key": "Application"
          }
        ]
      }
    },
    "Public1SubnetRouteTableAssociation": {
      "Type": "AWS::EC2::SubnetRouteTableAssociation",
      "Properties": {
        "SubnetId": {
          "Ref": "PublicSubnet1"
        },
        "RouteTableId": {
          "Ref": "PublicRouteTable"
        }
      }
    },
    "ALBListener": {
      "Type": "AWS::ElasticLoadBalancingV2::Listener",
      "Properties": {
        "Protocol": "HTTP",
        "DefaultActions": [
          {
            "TargetGroupArn": {
              "Ref": "ALBTargetGroup"
            },
            "Type": "forward"
          }
        ],
        "LoadBalancerArn": {
          "Ref": "ApplicationLoadBalancer"
        },
        "Port": "80"
      }
    },
    "ALBSecurityGroup": {
      "Type": "AWS::EC2::SecurityGroup",
      "Properties": {
        "SecurityGroupIngress": [
          {
            "ToPort": "80",
            "IpProtocol": "tcp",
            "CidrIp": "0.0.0.0/0",
            "FromPort": "80"
          }
        ],
        "GroupName": "http",
        "VpcId": {
          "Ref": "VPC"
        },
        "GroupDescription": "Enable HTTP access via port 80"
      }
    },
    "LaunchConfig": {
      "Type" : "AWS::AutoScaling::LaunchConfiguration",
      "Metadata" : {
        "AWS::CloudFormation::Init" : {
          "configSets" : {
            "wordpress_install" : ["install_cfn", "install_wordpress" ]
          },
          "install_cfn" : {
            "files": {
              "/etc/cfn/cfn-hup.conf": {
                "content": { "Fn::Join": [ "", [
                  "[main]\n",
                  "stack=", { "Ref": "AWS::StackId" }, "\n",
                  "region=", { "Ref": "AWS::Region" }, "\n"
                ]]},
                "mode"  : "000400",
                "owner" : "root",
                "group" : "root"
              },
              "/etc/cfn/hooks.d/cfn-auto-reloader.conf": {
                "content": { "Fn::Join": [ "", [
                  "[cfn-auto-reloader-hook]\n",
                  "triggers=post.update\n",
                  "path=Resources.LaunchConfig.Metadata.AWS::CloudFormation::Init\n",
                  "action=/opt/aws/bin/cfn-init -v ",
                  "         --stack ", { "Ref" : "AWS::StackName" },
                  "         --resource LaunchConfig ",
                  "         --configsets wordpress_install ",
                  "         --region ", { "Ref" : "AWS::Region" }, "\n"
                ]]},
                "mode"  : "000400",
                "owner" : "root",
                "group" : "root"
              }
            },
            "services" : {
              "sysvinit" : {
                "cfn-hup" : { "enabled" : "true", "ensureRunning" : "true",
                  "files" : ["/etc/cfn/cfn-hup.conf", "/etc/cfn/hooks.d/cfn-auto-reloader.conf"]}
              }
            }
          },

          "install_wordpress" : {
            "packages" : {
              "yum" : {
                "php"       : [],
                "php-mysql" : [],
                "mysql"     : [],
                "httpd"     : []
              }
            },
            "sources" : {
              "/var/www/html" : "http://wordpress.org/latest.tar.gz"
            },
            "files" : {
              "/tmp/create-wp-config" : {
                "content" : { "Fn::Join" : [ "", [
                  "#!/bin/bash\n",
                  "cp /var/www/html/wordpress/wp-config-sample.php /var/www/html/wordpress/wp-config.php\n",
                  "sed -i \"s/'database_name_here'/'",{ "Ref" : "DBName" }, "'/g\" wp-config.php\n",
                  "sed -i \"s/'username_here'/'",{ "Ref" : "DBUser" }, "'/g\" wp-config.php\n",
                  "sed -i \"s/'password_here'/'",{ "Ref" : "DBPassword" }, "'/g\" wp-config.php\n",
                  "sed -i \"s/'localhost'/'",{ "Fn::GetAtt" : [ "MySQLDatabase", "Endpoint.Address" ] }, "'/g\" wp-config.php\n"
                ]]},
                "mode" : "000500",
                "owner" : "root",
                "group" : "root"
              }
            },
            "commands" : {
              "01_configure_wordpress" : {
                "command" : "/tmp/create-wp-config",
                "cwd" : "/var/www/html/wordpress"
              }
            },
            "services" : {
              "sysvinit" : {
                "httpd" : { "enabled" : "true", "ensureRunning" : "true" }
              }
            }
          }
        }
      },
      "Properties": {
        "AssociatePublicIpAddress" : true,
        "ImageId" : { "Fn::FindInMap" : [ "AWSRegionArch2AMI", { "Ref" : "AWS::Region" },
          { "Fn::FindInMap" : [ "AWSInstanceType2Arch", { "Ref" : "InstanceType" }, "Arch" ] } ] },
        "InstanceType"   : { "Ref" : "InstanceType" },
        "SecurityGroups" : [ {"Ref" : "WebServerSecurityGroup"} ],
        "KeyName"        : { "Ref" : "KeyName" },
        "UserData" : { "Fn::Base64" : { "Fn::Join" : ["", [
          "#!/bin/bash -xe\n",
          "yum update -y aws-cfn-bootstrap\n",

          "/opt/aws/bin/cfn-init -v ",
          "         --stack ", { "Ref" : "AWS::StackName" },
          "         --resource LaunchConfig ",
          "         --configsets wordpress_install ",
          "         --region ", { "Ref" : "AWS::Region" }, "\n",

          "/opt/aws/bin/cfn-signal -e $? ",
          "         --stack ", { "Ref" : "AWS::StackName" },
          "         --resource WebServerGroup ",
          "         --region ", { "Ref" : "AWS::Region" }, "\n"
        ]]}}
      }
    },
    "DBSubnetGroup": {
      "Type": "AWS::RDS::DBSubnetGroup",
      "Properties": {
        "SubnetIds": [
          {
            "Ref": "PrivateSubnet1"
          },
          {
            "Ref": "PrivateSubnet2"
          }
        ],
        "DBSubnetGroupDescription": "This is a privateSubnet"
      }
    },
    "VPC": {
      "Type": "AWS::EC2::VPC",
      "Properties": {
        "CidrBlock": "10.0.0.0/16",
        "Tags": [
          {
            "Value": "Sample-VPC",
            "Key": "Name"
          },
          {
            "Value": {
              "Ref": "AWS::StackId"
            },
            "Key": "Application"
          }
        ]
      }
    },
    "AttachGateway": {
      "Type": "AWS::EC2::VPCGatewayAttachment",
      "Properties": {
        "VpcId": {
          "Ref": "VPC"
        },
        "InternetGatewayId": {
          "Ref": "InternetGateway"
        }
      }
    },
    "MySQLDatabase": {
      "Type": "AWS::RDS::DBInstance",
      "Properties": {
        "Engine": "MySQL",
        "MultiAZ": {
          "Ref": "MultiAZDatabase"
        },
        "MasterUsername": {
          "Ref": "DBUser"
        },
        "MasterUserPassword": {
          "Ref": "DBPassword"
        },
        "VPCSecurityGroups": [
          {
            "Fn::GetAtt": [
              "DBEC2SecurityGroup",
              "GroupId"
            ]
          }
        ],
        "AllocatedStorage": {
          "Ref": "DBAllocatedStorage"
        },
        "DBInstanceClass": {
          "Ref": "DBInstanceClass"
        },
        "DBSubnetGroupName": {
          "Ref": "DBSubnetGroup"
        },
        "DBName": {
          "Ref": "DBName"
        }
      }
    },
    "PublicRouteTable": {
      "Type": "AWS::EC2::RouteTable",
      "Properties": {
        "VpcId": {
          "Ref": "VPC"
        },
        "Tags": [
          {
            "Value": {
              "Ref": "AWS::StackId"
            },
            "Key": "Application"
          }
        ]
      }
    },
    "PrivateSubnet2": {
      "Type": "AWS::EC2::Subnet",
      "Properties": {
        "Tags": [
          {
            "Value": "PrivateSubnet1",
            "Key": "Name"
          },
          {
            "Value": {
              "Ref": "AWS::StackId"
            },
            "Key": "Application"
          }
        ],
        "VpcId": {
          "Ref": "VPC"
        },
        "CidrBlock": "10.0.3.0/24",
        "AvailabilityZone": {
          "Fn::Select": [
            "1",
            {
              "Fn::GetAZs": {
                "Ref": "AWS::Region"
              }
            }
          ]
        }
      }
    },
    "PrivateSubnet1": {
      "Type": "AWS::EC2::Subnet",
      "Properties": {
        "Tags": [
          {
            "Value": "PrivateSubnet1",
            "Key": "Name"
          },
          {
            "Value": {
              "Ref": "AWS::StackId"
            },
            "Key": "Application"
          }
        ],
        "VpcId": {
          "Ref": "VPC"
        },
        "CidrBlock": "10.0.2.0/24",
        "AvailabilityZone": {
          "Fn::Select": [
            "0",
            {
              "Fn::GetAZs": {
                "Ref": "AWS::Region"
              }
            }
          ]
        }
      }
    },
    "PublicSubnet2NetworkAclAssociation": {
      "Type": "AWS::EC2::SubnetNetworkAclAssociation",
      "Properties": {
        "SubnetId": {
          "Ref": "PublicSubnet2"
        },
        "NetworkAclId": {
          "Ref": "NetworkAcl"
        }
      }
    },
    "InboundPublicNetworkAclEntry": {
      "Type": "AWS::EC2::NetworkAclEntry",
      "Properties": {
        "NetworkAclId": {
          "Ref": "NetworkAcl"
        },
        "RuleNumber": "100",
        "Protocol": "-1",
        "Egress": "false",
        "RuleAction": "allow",
        "CidrBlock": "0.0.0.0/0"
      }
    },
    "WebServerSecurityGroup": {
      "Type": "AWS::EC2::SecurityGroup",
      "Properties": {
        "SecurityGroupIngress": [
          {
            "ToPort": "80",
            "IpProtocol": "tcp",
            "CidrIp": "0.0.0.0/0",
            "FromPort": "80"
          },
          {
            "ToPort": "22",
            "IpProtocol": "tcp",
            "CidrIp": {
              "Ref": "SSHLocation"
            },
            "FromPort": "22"
          }
        ],
        "VpcId": {
          "Ref": "VPC"
        },
        "GroupDescription": "Enable HTTP access via port 80 locked down to the ELB and SSH access"
      }
    },
    "WebServerGroup": {
      "Type": "AWS::AutoScaling::AutoScalingGroup",
      "Properties": {
        "DesiredCapacity": {
          "Ref": "WebServerCapacity"
        },
        "MinSize": "1",
        "MaxSize": "3",
        "VPCZoneIdentifier": [
          {
            "Ref": "PublicSubnet1"
          },
          {
            "Ref": "PublicSubnet2"
          }
        ],
        "LaunchConfigurationName": {
          "Ref": "LaunchConfig"
        },
        "TargetGroupARNs": [
          {
            "Ref": "ALBTargetGroup"
          }
        ]
      },
      "UpdatePolicy": {
        "AutoScalingRollingUpdate": {
          "PauseTime": "PT15M",
          "WaitOnResourceSignals": "true",
          "MaxBatchSize": "1",
          "MinInstancesInService": "1"
        }
      }
    },
    "ApplicationLoadBalancer": {
      "Type": "AWS::ElasticLoadBalancingV2::LoadBalancer",
      "Properties": {
        "Subnets": [
          {
            "Ref": "PublicSubnet1"
          },
          {
            "Ref": "PublicSubnet2"
          }
        ],
        "SecurityGroups": [
          {
            "Ref": "ALBSecurityGroup"
          }
        ],
        "Tags": [
          {
            "Value": "Sample-ALB",
            "Key": "Name"
          }
        ]
      }
    },
    "PublicSubnet1": {
      "Type": "AWS::EC2::Subnet",
      "Properties": {
        "Tags": [
          {
            "Value": "PublicSubnet1",
            "Key": "Name"
          },
          {
            "Value": {
              "Ref": "AWS::StackId"
            },
            "Key": "Application"
          }
        ],
        "VpcId": {
          "Ref": "VPC"
        },
        "CidrBlock": "10.0.0.0/24",
        "AvailabilityZone": {
          "Fn::Select": [
            "0",
            {
              "Fn::GetAZs": {
                "Ref": "AWS::Region"
              }
            }
          ]
        }
      }
    },
    "DBEC2SecurityGroup": {
      "Type": "AWS::EC2::SecurityGroup",
      "Properties": {
        "SecurityGroupIngress": [
          {
            "ToPort": "3306",
            "IpProtocol": "tcp",
            "SourceSecurityGroupId": {
              "Ref": "WebServerSecurityGroup"
            },
            "FromPort": "3306"
          }
        ],
        "VpcId": {
          "Ref": "VPC"
        },
        "GroupDescription": "Open database for access"
      }
    },
    "PublicSubnet2": {
      "Type": "AWS::EC2::Subnet",
      "Properties": {
        "Tags": [
          {
            "Value": "PublicSubnet2",
            "Key": "Name"
          },
          {
            "Value": {
              "Ref": "AWS::StackId"
            },
            "Key": "Application"
          }
        ],
        "VpcId": {
          "Ref": "VPC"
        },
        "CidrBlock": "10.0.1.0/24",
        "AvailabilityZone": {
          "Fn::Select": [
            "1",
            {
              "Fn::GetAZs": {
                "Ref": "AWS::Region"
              }
            }
          ]
        }
      }
    },
    "Public2SubnetRouteTableAssociation": {
      "Type": "AWS::EC2::SubnetRouteTableAssociation",
      "Properties": {
        "SubnetId": {
          "Ref": "PublicSubnet2"
        },
        "RouteTableId": {
          "Ref": "PublicRouteTable"
        }
      }
    },
    "OutboundPublicNetworkAclEntry": {
      "Type": "AWS::EC2::NetworkAclEntry",
      "Properties": {
        "NetworkAclId": {
          "Ref": "NetworkAcl"
        },
        "RuleNumber": "100",
        "Protocol": "-1",
        "Egress": "true",
        "RuleAction": "allow",
        "CidrBlock": "0.0.0.0/0"
      }
    },
    "ALBTargetGroup": {
      "Type": "AWS::ElasticLoadBalancingV2::TargetGroup",
      "Properties": {
        "HealthCheckIntervalSeconds": 10,
        "VpcId": {
          "Ref": "VPC"
        },
        "Protocol": "HTTP",
        "UnhealthyThresholdCount": 5,
        "HealthyThresholdCount": 2,
        "HealthCheckTimeoutSeconds": 5,
        "Port": 80
      }
    }
  },
  "Mappings": {
    "AWSInstanceType2Arch": {
      "t2.micro": {
        "Arch": "HVM64"
      },
      "t2.nano": {
        "Arch": "HVM64"
      },
      "t1.micro": {
        "Arch": "HVM64"
      }
    },
    "AWSRegionArch2AMI": {
      "us-west-2": {
        "HVM64": "ami-a0cfeed8",
        "HVMG2": "ami-0e09505bc235aa82d"
      },
      "us-east-1": {
        "HVM64": "ami-0ff8a91507f77f867",
        "HVMG2": "ami-0a584ac55a7631c0c"
      },
      "ap-northeast-1": {
        "HVM64": "ami-00a5245b4816c38e6",
        "HVMG2": "ami-053cdd503598e4a9d"
      },
      "us-west-1": {
        "HVM64": "ami-0bdb828fd58c52235",
        "HVMG2": "ami-066ee5fd4a9ef77f1"
      }
    }
  }
}

確認してみてOKだったら、変更セットを作成します。

# 変更セットの作成
aws cloudformation create-change-set --stack-name sample-stack \
--change-set-name sample-change-set --template-body file://`pwd`/VPC_Multi_RDS_AutoScaling_EC2_instance_wp.json \
--parameters ParameterKey=KeyName,ParameterValue=YOUR_KEY_NAME \
ParameterKey=SSHLocation,ParameterValue=xxx.xxx.xxx.xxx/32

変更セットを表示します。

# 変更セットの表示
aws cloudformation list-change-sets --stack-name sample-stack

表示した変更セット ID を指定して変更セットに関する情報を取得します。

(ARNじゃなきゃあかんです。下記のコマンドはsampleのarnです。)

# 変更セットの表示
aws cloudformation describe-change-set --change-set-name arn:aws:cloudformation:ap-northeast-1:123456789012:stack/sample-change-set/1a2345b6-0000-00a0-a123-00abc0abc000"

変更セットを実行します。 変更セットを実行すると、スタックに関連付けられた変更セットは AWS CloudFormation によってすべて削除されます。

# 変更セットの実行
aws cloudformation execute-change-set --change-set-name arn:aws:cloudformation:ap-northeast-1:123456789012:stack/sample-change-set/1a2345b6-0000-00a0-a123-00abc0abc000"

進行状況を確認します。

# 進行状況を確認
aws cloudformation describe-stacks --stack-name sample-stack

完了したら、変更セットを確認

# 変更セットを確認
aws cloudformation list-change-sets --stack-name sample-stack

f:id:moqrin3:20190126153345p:plain

更新が完了しているので、出力画面からALBのリンクに飛びます。

f:id:moqrin3:20190126153319p:plain

あ、あれ!? Apacheのトップページですけど、、、WP的な画面じゃないですね。。 何で?? (ちなみに、元々のサンプルをそのまま実行してみても何かうまくいきませんでした、、、主題ではないので、また後日、再度詳細の確認するようにしますー。。)

ということで、スタックを削除します。

# 変更セットを確認
aws cloudformation delete-stack --stack-name sample-stack

3. 宿題

  • CloudFormation ヘルパースクリプトの確認

  • テンプレートのネスト化

  • 何故かBlackBeltの資料には触られていないCloudFormerも利用してみたいですね。

所感

今回も実際に手を動かしてみて、色々と当惑しつつ、お勉強になりました。

  • NetworkACLをインバウンド&アウトバウンド含めて明示的に設定してあげないとダメとか。

  • DB用のSubnetを作成してあげないとダメとか。

  • 指定したVPC内にDB作成するには、DBSubnetGroupNameが必要とか。

参考:

AWS CloudFormation とは

AWS Black Belt / AWS CloudFormation

https://stackoverflow.com/questions/37038489/how-to-add-a-rds-instance-to-a-vpc-using-aws-cloudformationDBSubnetGroupDescription

想ひ出23: AWS/CloudFormationに入門する/1

f:id:moqrin3:20190114193742p:plain

おはようございます、moqrinです。

今更感満載ですが、CloudFormationに入門したので、その学習まとめ1です。

しばらく触らないと忘れるため、個人的後日振り返り用でございます。

なお、また後日、学習を続けてまとめるつもりでございます。

内容としては、公式リファレンスを組み換えたまとめです。

目次

1. AWS CloudFormationの仕組み

2. テンプレートの作成方法

3. テンプレートの基礎項目

4. 主な組み込み関数

5. その他の重要機能

6. AWS CLIの利用

7. CloudFormation ヘルパースクリプト


1. AWS CloudFormationの仕組み

  1. テンプレートを作成

  2. テンプレートをローカルまたは S3 バケットに保存。

    ローカルに保存されたテンプレートファイルを指定した場合、AWS CloudFormation はそれを AWS アカウントの S3 バケットにアップロードする

  3. テンプレートのファイルの場所 (ローカルコンピューターのパスや Amazon S3 の URL など) を指定し、AWS CloudFormation スタックを作成する。


用語

  • テンプレート --- JSON または YAML 形式のテキストファイル。AWS リソースを作成する際の設計図として使用する。

    リソースの依存関係はAWS CloudFormationで自動判別してくれる。

  • スタック --- テンプレートで作成されたリソースのコレクションを管理する単位。スタックを作成、更新、削除することで、リソースのコレクションを作成、更新、削除する。

    スタックを削除しても、その中のいくつかのリソースは保持したい場合には、削除ポリシーを使用してそのリソースを保持できる。

2. テンプレートの作成(利用)方法

  1. クイックスタート

    AWS ベストプラクティスに基づいた人気の高いソリューションを即座に利用できるクイックスタート)

  2. サンプルテンプレート & テンプレートスニペット

    サンプルからリファレンスを確認して、カスタマイズを施す

  3. ゼロからテンプレート書く

3. テンプレートの基礎項目

項目 内容
Resources ※ 必須項目 スタックを構成するリソースとプロパティを指定
Parameters 実行時にユーザ入力を求めるパラメータ値(Keypair名やDBユーザ名など)
Mappings キーと値のマッピング。パラメータ値の条件指定に使用
Outputs スタック構築後にAWS CloudFormationから出力させる値(DNSやEIPなど)
Transform サーバレスアプリケーションの場合に使用。使用するSAMのバージョンを指定
Description テンプレートの説明文
Metadata テンプレートに関する追加情報
AWSTemplateFormatVersion テンプレートのバージョン

4. 主な組み込み関数

関数 内容
!Ref テンプレート内の論理名(ID)から物理名(ID)を参照する
!GetAtt リソースが持つ属性値を取得する
!Join 文字列の結合
!GetAZs AZを取得する擬似パラメーター
Fn::ImportValue エクスポート値を取得する
Fn::Base64 Base64エンコード
Fn::If,Fn::Equals,Fn::Not 条件関数

5. その他の重要機能

  • DependsOn 属性: あるリソースに続けて別のリソースを作成する必要があることを指定する

6. AWS CLIの使用

AWS コマンドラインインターフェイスの使用

# テンプレートの検証
aws cloudformation validate-template --template-url file:///home/testuser/mytemplate.json
# 新規作成
aws cloudformation create-stack --stack-name myteststack --template-body file:///home/testuser/mytemplate.json --parameters ParameterKey=Parm1,ParameterValue=test1 ParameterKey=Parm2,ParameterValue=test2
# 新規作成/複数パラメータ
aws cloudformation create-stack --stack-name myteststack --template-body file:///home/testuser/mytemplate.json --parameters ParameterKey=Parm1,ParameterValue=test1 ParameterKey=SubnetIDs,ParameterValue=SubnetID1\\,SubnetID2
# 更新
aws cloudformation update-stack --stack-name myteststack --template-body file:///home/testuser/mytemplate.json --parameters ParameterKey=Parm1,ParameterValue=test1 ParameterKey=Parm2,ParameterValue=test2
# 削除
aws cloudformation delete-stack --stack-name myteststack

7. CloudFormation ヘルパースクリプト

Amazon EC2 インスタンスでソフトウェアをインストールしたりサービスを開始したりするために使用できる、Python ヘルパースクリプト

後日の宿題。。。

CloudFormation ヘルパースクリプトリファレンス

参考:

AWS CloudFormation とは

AWS Black Belt / AWS CloudFormation

想ひ出22: 小ネタ/Grafana5.3/G Suiteメール送信 備忘録

f:id:moqrin3:20181201184019j:plain

おはようございます、moqrinです。

今記事は、Grafanaをいじっている際に発生したメール送信のための設定の備忘録です。
PostfixからG Suite経由でメール送信をします。

こちらの記事で設定していますが、記事が更新されていないようで、
完全にマッチはしていなそうです。

SMTP リレー: Gmail 以外の送信メールを Google 経由にする

やること

1. [Apps] > [G Suite] > [Gmail] > [詳細設定] にアクセスする
2. 包括的なメール ストレージをオンにする
3. [SMTP リレーサービス] 設定までスクロールし、カーソルを合わせて [設定]する
4. /etc/postfix/main.cfを編集する
5. /etc/grafana/grafana.iniを編集する
6. 確認

1. [Apps] > [G Suite] > [Gmail] > [詳細設定] にアクセスする

参照記事通りに、アクセスします。

2. 包括的なメール ストレージをオンにする

f:id:moqrin3:20181201183923p:plain

3. [SMTP リレーサービス] 設定までスクロールし、カーソルを合わせて [設定]する

設定の選択肢はいくつかあるのですが、今回はドメイン内のアドレスからのみ送信許可としました。

f:id:moqrin3:20181201183937p:plain

4. /etc/postfix/main.cfを編集する

いちおう原本は残しておきます。

cp /etc/postfix/main.cf /etc/postfix/main.cf.org
# vim /etc/postfix/main.cf

myhostname = grafana.YOUR_DOMAIN_NAME
mydomain = YOUR_DOMAIN_NAME
myorigin = $myhostname
relayhost = smtp-relay.gmail.com:587

5. /etc/grafana/grafana.iniを編集する

# vim /etc/grafana/grafana.ini

[smtp]
- enabled = false
+ enabled = true
host = localhost:25
from_address = grafana@YOUR_DOMAIN_NAME

6. 確認

f:id:moqrin3:20181201184004p:plain

わーい、来ましたー。

参考:

SMTP リレー: Gmail 以外の送信メールを Google 経由にする

想ひ出21: GCP/CloudSQL/Nginx/Zabbix4.0/Grafana5.3/Slack

f:id:moqrin3:20181201181427p:plain

おはようございます、moqrinです。

今記事は、Zabbix4.0がリリースされたそうなので、
なるほど。とりあえずGCPで設定してみた、というだけの備忘録です。

基本的には下記の素晴らしい諸先輩方に依っておりますので、
わたしのクソブログでなくてもOKです!

だいたい下記、手動で対応して45分くらいですかね。。

やること

1. 準備しよう
2. Zabbix,MySQL5.7のインストール
3. Zabbix データベースとユーザーを作成
4. Zabbix-server設定
5. Zabbix-webの権限周り設定
6. Nginx php-fpmの設定
7. Zabbixの設定
8. Grafana導入
9. Zabbixエージェントの設定
10. (ついでに)Slackアラート

1. 準備しよう

とりあえずCloudSQLを作成しておきましょう。(ちょっと時間かかるしね。)
(ZabbixサーバーとSQLインスタンスのリージョン&ゾーンを合わせておくとローカルIPで接続できるね!)

VPCファイヤーウォールの設定を確認しておきます。

Port Use Server
80 or 443 http or https Zabbixサーバー
3000 Grafana Zabbixサーバー
10050 Zabbixサーバー → Zabbixエージェント 監視サーバー(Zabbixサーバー含む)
10051 Zabbixエージェント → Zabbixサーバー Zabbixサーバー

Remiリポジトリのインストールします。

rpm -ivh http://rpms.famillecollet.com/enterprise/remi-release-7.rpm

php7.2を利用するためにリポジトリ編集しておきます。(php7.3でも良いけど。)

# vim /etc/yum.repos.d/remi.repo
- enabled=0
+ enabled=1
# vim /etc/yum.repos.d/remi-php72.repo
- enabled=0
+ enabled=1

悪さするみたいなので、SELINUXさんを落とします。

# vim /etc/sysconfig/selinux
- SELINUX=enforcing
+ SELINUX=disabled

そして再起動。

reboot

(いちおう確認しときますか。)

getenforce

2. Zabbix,MySQL5.7のインストール

Zabbix4.0インストールします。
参考URL:
Install and configure Zabbix server

rpm -ivh https://repo.zabbix.com/zabbix/4.0/rhel/7/x86_64/zabbix-release-4.0-1.el7.noarch.rpm
yum -y install zabbix-server-mysql zabbix-web-mysql zabbix-web-japanese zabbix-agent zabbix-get

mysql5.7インストールしまーす。(CloudSQLがMySQL5.7だから)

rpm -Uvh http://dev.mysql.com/get/mysql57-community-release-el7-11.noarch.rpm
yum install -y --enablerepo=mysql57-community mysql-community-server

確認しときます。

zabbix_server -V

3. Zabbix データベースとユーザーを作成

Cloud SQLが接続できるようになっているか、設定を確認しておきます。

f:id:moqrin3:20181201182031p:plain

DBに接続します。

mysql -u root  -h CLOUD_SQL_IP_ADDRESS -p

データベースおよびユーザーを作成します。

CREATE DATABASE zabbix character set utf8 collate utf8_bin;
CREATE USER zabbix@'%'IDENTIFIED BY 'YOUR_DB_PASSWORD';
GRANT ALL ON zabbix.* TO zabbix@'%';

初期データ、スキーマを投入します。

cd /usr/share/doc/zabbix-server-mysql-4.0.1
zcat create.sql.gz | mysql -u zabbix -h Cloud_SQL_IP_ADDRESS -p zabbix

4. Zabbix-server設定

# vim /etc/zabbix/zabbix_server.conf

# DBHost=localhost
DBHost=CLOUD_SQL_IP_ADDRESS

...

DBPassword=YOUR_DB_PASSWORD

起動しちゃいましょう。

systemctl start zabbix-server ; systemctl enable zabbix-server
systemctl status zabbix-server

ついでにエージェントも起動。

systemctl start zabbix-agent ; systemctl enable zabbix-agent
systemctl status zabbix-agent

# 確認
zabbix_agentd -V

Zabbixサーバー自身からエージェントバージョンを確認します。

zabbix_get -s 127.0.0.1 -k agent.version

5. Zabbix-webの権限周り設定

必要なものインストール。

 yum -y install nginx php-fpm wget

デフォルトapacheからnginx仕様に変更していきます。

ls -la /var/lib/php/
cd /var/lib/php/
chown -R nginx:nginx ./*
cd /etc/zabbix
ls -ld web
chown  nginx:nginx web
cd /var/www/html
ln -s /usr/share/zabbix .

6. Nginx php-fpmの設定

nginx設定

# vim /etc/nginx/conf.d/zabbix.conf

server {
        server_name YOUR_HOST_NAME;

        root        /var/www/html/zabbix;
        index       index.html index.php;
        access_log  /var/log/nginx/zabbix/access.log main;
        error_log   /var/log/nginx/zabbix/error.log error;

        location ~ \.php {
                fastcgi_pass    127.0.0.1:9000;
                fastcgi_index   index.php;
                fastcgi_param   SCRIPT_FILENAME $document_root$fastcgi_script_name;
                include fastcgi_params;
        }
}

php-fpm設定

# vim /etc/php-fpm.d/www.conf

-user = apache
+user = nginx

-group = apache
+group = nginx

php.ini設定
※ やっておかないとこんな感じで後でZabbixに怒られます。

f:id:moqrin3:20181201182219p:plain

# vim /etc/php.ini

-max_execution_time = 30
+max_execution_time = 300

-max_input_time = 60
+max_input_time = 300

-post_max_size = 8M
+post_max_size = 32M

+date.timezone = Asia/Tokyo

それぞれ起動します。

systemctl enable php-fpm ; systemctl start php-fpm
systemctl enable nginx ; systemctl start nginx

ホスト設定してURL叩きます。

# vim /etc/hosts
ZABBIX_SEVER_IP_ADDRES YOUR_HOST_NAME

7. Zabbixの設定

初期ログインは、下記です。

Username:Admin
Password:zabbix

何か問題があったらこちらを確認するだす!

tailf /var/log/zabbix/zabbix_server.log

設定を進めていきます。

f:id:moqrin3:20181201182254p:plain

パスワードと言語を更新しておきますー。

f:id:moqrin3:20181201182325p:plain

8. Grafana導入

次は、ZabbixのデータをGrafanaで見れるようにします。

参考URL:
Download Grafana
Grafana-Zabbix plugin Installation

とりあえず記載されている通り、Grafanaをインストール。

wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.3.2-1.x86_64.rpm
yum localinstall -y grafana-5.3.2-1.x86_64.rpm

Grafana-Zabbixプラグインをインストールして起動させます。

grafana-cli plugins list-remote
grafana-cli plugins install alexanderzobnin-zabbix-app
systemctl restart grafana-server ; systemctl enable grafana-server

ZabbixでGrafanaグループを作成して権限を付与してユーザー設定します。
(この例だとZabbix serversしか閲覧付与していないけど。。)

f:id:moqrin3:20181201182427p:plain

f:id:moqrin3:20181201182506p:plain

f:id:moqrin3:20181201182527p:plain

Grafanaの設定をします。(ホスト名:3000です。)
初期ログインIDとパスワードは「admin」です。

f:id:moqrin3:20181201182555p:plain

f:id:moqrin3:20181201182612p:plain

f:id:moqrin3:20181201182642p:plain

参考URL:
Zabbixの監視情報をGrafanaでグラフィカルなダッシュボードにしてみた

なお、/grafanaでgrafanaログインに遷移したい場合は下記を追記します。

# /etc/nginx/conf.d/nginx.conf

location /grafana/ {
     proxy_pass         http://localhost:3000/;
     proxy_set_header   Host $host;
}
# vim /etc/grafana/grafana.ini

[server]
root_url = %(protocol)s://%(domain)s/grafana/

9. Zabbixエージェントの設定

監視対象となるサーバーで対応していきます。
Zabbix4のリポジトリを追加してエージェントをインストールします。
(これはサーバー構築するときにAnsibleで対応しちゃえばいいねー・・・)

参考URL:
VirtualBox Zabbix3.0 エージェント構築

rpm -ivh https://repo.zabbix.com/zabbix/4.0/rhel/7/x86_64/zabbix-release-4.0-1.el7.noarch.rpm
yum -y install zabbix-agent

エージェントの設定を編集していきます。

# vim /etc/zabbix/zabbix_agentd.conf

# Zabbixサーバからのリモートコマンドを許可
- EnableRemoteCommands=0
+ EnableRemoteCommands=1

# ZabbixサーバのIPアドレスを設定
- Server=127.0.0.1
+ Server=ZABBIX_SEVER_IP

- ServerActive=127.0.0.1
+ ServerActive=ZABBIX_SEVER_IP

Zabbixエージェントの起動と自動起動設定

systemctl enable zabbix-agent ; systemctl start zabbix-agent

# 確認
zabbix_agentd -V

Zabbixサーバ 疎通テスト

Zabbixサーバー側で下記コマンドを実行し、 Zabbixエージェント(192.168.33.10)のバージョンが表示されれば疎通OK。

zabbix_get -s 192.168.33.10 -k agent.version

Zabbixにホストを登録

f:id:moqrin3:20181201182735p:plain

f:id:moqrin3:20181201182749p:plain

10. Slackアラート

Slackにアラート流してみる。

参考URL:
[zabbix2][zabbix3]slackと連携してみたよ

Webサーバー停止する。

systemctl stop nginx

f:id:moqrin3:20181201182821p:plain

f:id:moqrin3:20181201182848p:plain

はーい、来ましたー。

参考:

Install and configure Zabbix server
Zabbix 4.0 インストールメモ(CentOS7 + MySQL 8.0)
CentOS 6.5でzabbix+nginx+php-fpm環境を構築してみる
Download Grafana
Grafana-Zabbix plugin Installation
Zabbixの監視情報をGrafanaでグラフィカルなダッシュボードにしてみた
VirtualBox Zabbix3.0 エージェント構築
[zabbix2][zabbix3]slackと連携してみたよ
How to proxy /grafana with nginx?

想ひ出20: GCP/Flask1.0/ローカルからCloudStorageにアップロードしてみる

f:id:moqrin3:20181201092259p:plain
GCP CloudStorage

おはようございます、moqrinです。

前々回に、GCEにデプロイしたFlaskのチュートリアルをCloudSQLと接続しました。
今度はCloudStorageを使ってローカルからUploadしてみようぜ、というお話です。
なお、基本的に過去の記事からの延長なのでご留意下さい。

やること

1. モデルを修正してマイグレーション
2. CloudStorageを作成してallUsersで閲覧OKにする
3. サービスアカウントキーを作成して手元に保存
4. 環境変数にサービスアカウントの情報を設定する
5. configにプロジェクトIDを設定
6. storage.pyを作成
7. Blueprintを修正
8. 画面フォームを修正
9. 確認

1. モデルを修正してマイグレーション

テーブルに画像保存するURLと画像名のColumnを追加します。

# app/models.py

from app import db, login_manager
from flask_login import UserMixin

class User(UserMixin, db.Model):

    __tablename__ = 'user'

    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(60), nullable=False, unique=True)
    password = db.Column(db.String(128), nullable=False)


@login_manager.user_loader
def load_user(user_id):
    return User.query.get(int(user_id))

class Post(db.Model):

    __tablename__ = 'post'

    id = db.Column(db.Integer, primary_key=True)
    author_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
    created = db.Column(db.TIMESTAMP, nullable=False)
    title = db.Column(db.String(200), nullable=False)
    body = db.Column(db.String(200), nullable=False)
    """
    追記
    """
    file_url = db.Column(db.TEXT(), nullable=False)
    filename = db.Column(db.String(200), nullable=False)

下記コマンドでマイグレーション対応完了です。
すでにテーブルがある場合はテーブル削除してから実行します。

flask db init
flask db migrate
flask db upgrade

2. CloudStorageを作成してallUsersで閲覧OKにする

Storageを作成して、allUsersで閲覧OKのメンバー追加します。
設定参考

3. サービスアカウントキーを作成して手元に保存

GCP外のアプリケーションからGoogle Cloud Storage API を有効化してアクセスするためには、サービスアカウントキーが必要となります。
Storageの読み書きできる権限で作成して、お手元にダウンロードします。

Cloud Storage Client Libraries

4. 環境変数にサービスアカウントキーを設定する

アクセス許可してもらうためにサービスアカウントキーをOSに設定しておきます。

export GOOGLE_APPLICATION_CREDENTIALS=path/to/ServiceAccountJsonKey

5. configにStorageが作成されたプロジェクトIDを設定

手っ取り早くこんな感じで設定しています。

# instance/config.py
PROJECT_ID = 'moqrin-storage'

6. storage.pyを作成

ほぼチュートリアルそのままで利用しています。

# app/storage.py

from __future__ import absolute_import

import os
os.getenv("GOOGLE_APPLICATION_CREDENTIALS")

import datetime

from google.cloud import storage
import six
from werkzeug import secure_filename
from werkzeug.exceptions import BadRequest

from app import ALLOWED_EXTENSIONS, PROJECT_ID, CLOUD_STORAGE_BUCKET


def _get_storage_client():
    return storage.Client(
        project=PROJECT_ID)

def _check_extension(filename, allowed_extensions):
    if ('.' not in filename or
            filename.split('.').pop().lower() not in allowed_extensions):
        raise BadRequest(
            "{0} has an invalid name or extension".format(filename))


def _safe_filename(filename):

    filename = secure_filename(filename)
    date = datetime.datetime.utcnow().strftime("%Y-%m-%d-%H%M%S")
    basename, extension = filename.rsplit('.', 1)
    return "{0}-{1}.{2}".format(basename, date, extension)


def upload_file(file_stream, filename, content_type):

    _check_extension(filename, ALLOWED_EXTENSIONS)
    filename = _safe_filename(filename)

    client = _get_storage_client()
    bucket = client.bucket(CLOUD_STORAGE_BUCKET)

    blob = bucket.blob(filename)

    blob.upload_from_string(
        file_stream,
        content_type=content_type)

    url = blob.public_url

    if isinstance(url, six.binary_type):
        url = url.decode('utf-8')

    print(url)

    return url

7. Blueprintを修正

下記の公式ドキュメントを参考にアップロード機能を実装していきます。
追記の部分を記載していきます。元はこちらです。

参考: Flask - Uploading Files

# app/__init__.py
...

ALLOWED_EXTENSIONS = set(['txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif'])
PROJECT_ID = PROJECT_ID
CLOUD_STORAGE_BUCKET = 'YOUR-BUCKET-NAME'

...

def create_app(test_config=None):

...

    return app
# app/blog.py

from flask import (
    Blueprint, flash, g, redirect, render_template, request, url_for
)

from app import storage
from . import db, ALLOWED_EXTENSIONS
...

bp = Blueprint('blog', __name__)


def allowed_file(filename):
    return '.' in filename and \
           filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS

def upload_image_file(file):

    if not file:
        return None

    public_url = storage.upload_file(
        file.read(),
        file.filename,
        file.content_type
    )

    return public_url

...

@bp.route('/create', methods=('GET', 'POST'))
@login_required
def create():
    if request.method == 'POST':
        title = request.form['title']
        body = request.form['body']
        error = None

        if not title:
            error = 'Title is required.'

        if 'img_file' not in request.files:
            flash('No file part')
            return redirect(request.url)
        file = request.files['img_file']

        image_url = upload_image_file(request.files.get('img_file'))

        if error is not None:
            flash(error)
        else:
            post = Post(title=title,
                        body=body,
                        author_id=g.user.id,
                        file_url=image_url,
                        filename=file.filename
                        )
...                        

@bp.route('/<int:id>/update', methods=('GET', 'POST'))
@login_required
def update(id):
    post = Post.query.get_or_404(id)

    if request.method == 'POST':
        title = request.form['title']
        body = request.form['body']
        error = None

        if not title:
            error = 'Title is required.'

        if error is not None:
            flash(error)

        if 'img_file' in request.files:
            file = request.files['img_file']

            if file and allowed_file(file.filename):
                image_url = upload_image_file(request.files.get('img_file'))
            post.id = id
            post.title = title
            post.body = body
            post.file_url = image_url
            post.filename = file.filename
            db.session.commit()
            flash('Successfully edited the post.')

        else:
            post.id = id
            post.title = title
            post.body = body
            db.session.commit()
...               

8. 画面フォームを修正

画像表示およびアップロードができるように修正します。

# app/templates/blog/index.html

{% extends 'base.html' %}

{% block header %}
<h1>{% block title %}Posts{% endblock %}</h1>
{% if current_user.is_authenticated %}
<a class="action" href="{{ url_for('blog.create') }}">New</a>
{% endif %}
{% endblock %}

{% block content %}
{% for post in posts %}
<article class="post">
    <header>
        <div>
            <h1>{{ post.title }}</h1>
            <div class="about">by {{ post.username }} on {{ post.created.strftime('%Y-%m-%d') }}</div>
        </div>
        <a class="action" href="{{ url_for('blog.update', id=post.id) }}">Edit</a>
    </header>
    <p class="body">{{ post.body }}</p>
    <img src="{{ post.file_url }}" width="400" height="200">
</article>
{% if not loop.last %}
<hr>
{% endif %}
{% endfor %}
{% endblock %}
# app/templates/blog/create.html

{% extends 'base.html' %}

{% block header %}
<h1>{% block title %}New Post{% endblock %}</h1>
{% endblock %}

{% block content %}
<form method="POST" enctype="multipart/form-data">
    <label for="title">Title</label>
    <input name="title" id="title" value="{{ request.form['title'] }}" required>
    <label for="body">Body</label>
    <textarea name="body" id="body">{{ request.form['body'] }}</textarea>
    <input type="file" id="img_file" name="img_file">
    <input type="submit" value="Save">
</form>
{% endblock %}
# app/templates/blog/update.html

{% extends 'base.html' %}

{% block header %}
<h1>{% block title %}Edit "{{ post['title'] }}"{% endblock %}</h1>
{% endblock %}

{% block content %}
<form method="post" enctype="multipart/form-data">
    <label for="title">Title</label>
    <input name="title" id="title"
           value="{{ request.form['title'] or post['title'] }}" required>
    <label for="body">Body</label>
    <textarea name="body" id="body">{{ request.form['body'] or post['body'] }}</textarea>
    <img src="{{ post.file_url }}" width="400" height="200">
    <input type="file" id="img_file" name="img_file">
    <input type="submit" value="Save">
</form>
<hr>
<form action="{{ url_for('blog.delete', id=post['id']) }}" method="post">
    <input class="danger" type="submit" value="Delete" onclick="return confirm('Are you sure?');">
</form>
{% endblock %}

9. 確認

Google CloudのAPI Client libraryをインストール

pip install gcloud

起動。

export FLASK_APP=run
export FLASK_ENV=development
flask run

表示を確認して喜びます。

f:id:moqrin3:20181201163743p:plain

わーい。 でも...やっぱり何だか動作おせーー。。。w

参考:

Python での Cloud Storage の使用
Python Client for Google Cloud Storage