20201118のAWSに関する記事は15件です。

AWS Fargate使ってみた

こんにちは。むんです:grinning:
AWS FargateでDockerを動かしてみたので、備忘録です。

目次

  • AWS Fargateとは?
  • Fargateでコンテナを作る
  • まとめ

AWS Fargateとは?

端的に言うと、サーバーレスでコンテナが使える技術。

Fargateでコンテナを作る

AWS公式チュートリアル
https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/userguide/fargate-getting-started.html

1. Amazon ECS コンソールの初回実行ウィザードを開く
2. ナビゲーションバーから、米国東部(バージニア北部) リージョンを選択する

現在、Fargateは米国東部(バージニア北部) リージョンでのみ、使える機能なようです。

3. コンテナを選択する

今回はnginxを選択しました。
Screen Shot 2020-11-18 at 4.13.44 PM.png

4. Serviceを選択

Load Balancerは今回起動してみたいだけなので、Noneを選択。
Screen Shot 2020-11-18 at 4.18.29 PM.png

5. Clusterを選択

Cluster nameも特にこだわらないので、defaultのままでNext。
Screen Shot 2020-11-18 at 4.22.57 PM.png

6. 確認、起動

確認画面に進むので、確認して「Create」
Screen Shot 2020-11-18 at 4.30.00 PM.png

7. ダッシュボードを確認する

「View Service」を選択。
下記のような画面が表示される。
赤枠のTaskをクリック。
Screen Shot 2020-11-18 at 4.40.45 PM.png
ENI Idをクリック。
Screen Shot 2020-11-18 at 4.37.34 PM.png
Public IP4 addressが記載されていることを確認する。
Screen Shot 2020-11-18 at 4.37.28 PM.png

8. ブラウザにて確認

ブラウザにPublic IP4 addressを入力する。
Screen Shot 2020-11-18 at 4.36.50 PM.png
起動できてますね!

まとめ

AWS Forgateを使用してみました。
ちょっと色々まだ謎なままですが・・・。
これからもう少しいじってみたいと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

CloudFormationで、GetAZs で取得できるAZについて

EC2-VPCでは、デフォルトのサブネットがあるアベイラビリティーゾーンのみを返します。 らしい。

EC2-VPC プラットフォームでは、Fn::GetAZs 関数はデフォルトのサブネットがあるアベイラビリティーゾーンのみを返します。デフォルトのサブネットがあるアベイラビリティーゾーンがない場合は、すべてのアベイラビリティーゾーンを返します。

先に デフォルトのサブネット を作っておくこと

aws ec2 create-default-subnet --availability-zone ap-northeast-1[a|c|d]

参考URL

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

俺でもわかるトレースID(AWS CloudFrontとAWS ELB)

俺です

いつでもCFやELBぶっ刺してもばっちこいなNginxになれるようにlog_formatに脳死でいれとけメモです

HTTPヘッダ Nginx log_format
X-Amz-Cf-Id $http_x_amz_cf_id
X-Amzn-Trace-Id $http_x_amzn_trace_id

https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/RequestAndResponseBehaviorCustomOrigin.html
https://aws.amazon.com/jp/premiumsupport/knowledge-center/trace-elb-x-amzn-trace-id/

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Go で AWS のプライベート VPC リソースにアクセスする

やりたいこと

AWS Systems Manager 経由で SSH トンネルを使用してプライベート VPC リソースにアクセスしたいと考えています。どうすればよいですか?

こちらの記事で紹介されているように、AWS Systems Manager Session Manager を利用することで、VPC 内に用意した踏み台サーバ経由でプライベート VPC 内のリソース (RDS など) にアクセスすることができます。

本来は SSH Client と AWS CLI を組み合わせて SSH トンネリング (ポートフォワーディング) を行うのですが、今回はこれを Go 言語と aws-sdk-go でやってみようと思います。
これにより、いちいち ssh コマンドを叩かずにプライベート VPC リソースに対してプログラムを実行できるのでかっこいいです(たぶん)。

検証環境

VPC 内の Private Subnet に RDS インスタンス (MySQL) と踏み台用の EC2 インスタンスがある環境を想定します。

bastion.png

図のようにローカル PC から Session Manager 経由で踏み台サーバに接続し、最終的にプライベートな RDS インスタンスに対して SHOW DATABASES を実行することをゴールとします。

上記の検証環境を再現する CloudFormation テンプレートを用意したのでお手元で試したい方は下記の詳細をご参照ください。

詳細

次のテンプレートを使用して CloudFormation スタックを作成すると検証環境を作成できます(ap-northeast-1 限定)。

AWSTemplateFormatVersion: 2010-09-09
Description: Create private RDS and bastion instance in VPC

Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: VPC Configuration
        Parameters:
          - VPCCIDR
          - PrivateSubnetACIDR
          - PrivateSubnetCCIDR
      - Label:
          default: DB Configuration
        Parameters:
          - DBMasterUsername
          - DBMasterPassword
      - Label:
          default: Bastion Configuration
        Parameters:
          - BastionKeyPair
          - BastionImageId

    ParameterLabels:
      VPCCIDR:
        default: VPC CIDR
      PrivateSubnetACIDR:
        default: Private Subnet A CIDR
      PrivateSubnetCCIDR:
        default: Private Subnet C CIDR
      DBMasterUsername:
        default: Database Master Username
      DBMasterPassword:
        default: Database Master Password
      BastionKeyPair:
        default: Bastion Server Key Pair Name
      BastionImageId:
        default: Bastion Server Image ID (DO NOT CHANGE)

Parameters:
  VPCCIDR:
    Type: String
    Default: 10.1.0.0/24
  PrivateSubnetACIDR:
    Type: String
    Default: 10.1.0.1/26
  PrivateSubnetCCIDR:
    Type: String
    Default: 10.1.0.64/26
  DBMasterUsername:
    Type: String
    Default: root
  DBMasterPassword:
    Type: String
  BastionKeyPair:
    Type: String
  BastionImageId:
    Type: AWS::SSM::Parameter::Value<String>
    Default: /aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2

Resources:
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref VPCCIDR
      EnableDnsSupport: true
      EnableDnsHostnames: true
      InstanceTenancy: default
      Tags:
        - Key: Name
          Value: ssm-bastion-example-vpc

  PrivateSubnetA:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: ap-northeast-1a
      CidrBlock: !Ref PrivateSubnetACIDR
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: ssm-bastion-example-private-subnet-a

  PrivateSubnetC:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: ap-northeast-1c
      CidrBlock: !Ref PrivateSubnetCCIDR
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: ssm-bastion-example-private-subnet-c

  PrivateRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: ssm-bastion-example-private-route

  PrivateSubnetRouteTableAssociationA:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PrivateSubnetA
      RouteTableId: !Ref PrivateRouteTable

  PrivateSubnetRouteTableAssociationC:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PrivateSubnetC
      RouteTableId: !Ref PrivateRouteTable

  VPCEndpointSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Serucity group for vpc endpoint
      VpcId: !Ref VPC
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 443
          ToPort: 443
          CidrIp: !Ref VPCCIDR

  VPCEndpointSSM:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      ServiceName: com.amazonaws.ap-northeast-1.ssm
      VpcEndpointType: Interface
      VpcId: !Ref VPC
      SubnetIds:
        - !Ref PrivateSubnetA
      SecurityGroupIds:
        - !Ref VPCEndpointSecurityGroup
      PrivateDnsEnabled: true

  VPCEndpointSSMMessages:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      ServiceName: com.amazonaws.ap-northeast-1.ssmmessages
      VpcEndpointType: Interface
      VpcId: !Ref VPC
      SecurityGroupIds:
        - !Ref VPCEndpointSecurityGroup
      SubnetIds:
        - !Ref PrivateSubnetA
      PrivateDnsEnabled: true

  VPCEndpointEC2Messages:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      ServiceName: com.amazonaws.ap-northeast-1.ec2messages
      VpcEndpointType: Interface
      VpcId: !Ref VPC
      SecurityGroupIds:
        - !Ref VPCEndpointSecurityGroup
      SubnetIds:
        - !Ref PrivateSubnetA
      PrivateDnsEnabled: true

  VPCEndpointS3:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      ServiceName: com.amazonaws.ap-northeast-1.s3
      VpcEndpointType: Gateway
      VpcId: !Ref VPC
      RouteTableIds:
        - !Ref PrivateRouteTable

  BastionRole:
    Type: AWS::IAM::Role
    Properties:
      Description: EC2 role for SSM
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - ec2.amazonaws.com
            Action:
              - sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore

  BastionInstanceProfile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      Roles:
        - !Ref BastionRole

  BastionSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Security group for bastion server
      GroupName: ssm-bastion-example-bastion-sg
      VpcId: !Ref VPC

  BastionServer:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: !Ref BastionImageId
      InstanceType: t2.micro
      SubnetId: !Ref PrivateSubnetA
      SecurityGroupIds:
        - !Ref BastionSecurityGroup
      IamInstanceProfile: !Ref BastionInstanceProfile
      KeyName: !Ref BastionKeyPair
      Tags:
        - Key: Name
          Value: ssm-bastion-example-bastion-server

  DBSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Security group for bastion exmaple db
      GroupName: ssm-bastion-example-db-sg
      VpcId: !Ref VPC
      SecurityGroupIngress:
        - SourceSecurityGroupId: !Ref BastionSecurityGroup
          IpProtocol: tcp
          FromPort: 3306
          ToPort: 3306

  DBSubnetGroup:
    Type: AWS::RDS::DBSubnetGroup
    Properties:
      DBSubnetGroupDescription: DB subnet group for bastion example db
      DBSubnetGroupName: ssm-bastion-example-db-sng
      SubnetIds:
        - !Ref PrivateSubnetA
        - !Ref PrivateSubnetC
      Tags:
        - Key: Name
          Value: ssm-bastion-example-db-sng

  DB:
    Type: AWS::RDS::DBInstance
    Properties:
      DBInstanceClass: db.t2.micro
      Engine: MySQL
      AllocatedStorage: 5
      PubliclyAccessible: false
      DBSubnetGroupName: !Ref DBSubnetGroup
      VPCSecurityGroups:
        - !GetAtt DBSecurityGroup.GroupId
      MasterUsername: !Ref DBMasterUsername
      MasterUserPassword: !Ref DBMasterPassword

Outputs:
  BastionInstanceId:
    Description: Bastion server instance id
    Value: !Ref BastionServer
  DBEndpoint:
    Description: Database endpoint
    Value: !GetAtt DB.Endpoint.Address

スタック作成時に次のパラメータを適切に設定してください。

  • VPC CIDR
    • 作成する VPC の CIDR ブロックです
    • 既存の VPC とぶつかる場合は適切な値に変更してください
  • Private Subnet A CIDR, Private Subnet C CIDR
    • 作成する Private Subnet の CIDR ブロックです
    • VPC CIDR を変更した場合はこちらも適切な値に変更してください
  • Database Master Password
    • 作成する RDS インスタンスのマスタパスワードです
  • Bastion Server Key Pair Name
    • 踏み台サーバに接続するためのキーペア名です
    • キーペアは予め作成し、秘密鍵をローカル PC にダウンロードしておいてください

どうやって実装するか

SSH Client と AWS CLI の処理を Go で書ければ実現可能なはずです。

SSH Client + AWS CLI の場合の処理の流れは次の通りです。

  1. SSH Client から ProxyCommand として aws ssm start-session を実行する
    1. AWS CLI は SSM の StartSession を呼んでセッションを開始する
    2. StartSession のレスポンスとして得られた URL とトークンを使って踏み台インスタンスと WebSocket で通信する
  2. SSH Client が ProxyCommand の通信を利用して RDS のポートをローカルポートにフォワーディングする

SSH Client の処理は golang.org/x/crypto/ssh パッケージを利用することで実装可能です。

問題は AWS CLI の処理です。
aws-sdk-goSSM.StartSession() で WebSocket 通信用の URL とトークンを得ることができますが、その後の WebSocket 通信の仕様が明らかにされていないのでどう使えばよいのか全くの不明です。

AWS CLI の実装を見ると、 Boto3 の SSM.Client.start_session で得られた出力を session-manager-plugin に渡していることが分かります。

https://github.com/aws/aws-cli/blob/2.1.10/awscli/customizations/sessionmanager.py

どうやら WebSocket 通信はこの session-manager-plugin に任せているようです(session-manager-plugin はバイナリ形式で配布されているため実装の詳細は不明)。

WebSocket 通信の仕様を頑張って解読するのは不毛な上、いつ変更されるかもわからないので今回は AWS CLI 同様に session-manager-plugin を呼び出す形で実装することにします。
AWS CLI と全く同じ呼び出し方をしてあげれば問題なく使えるはずです。
また、AWS CLI と session-manager-plugin 間のインタフェースは互換性を保つためにそう簡単には変更されないものと予想されます。

実装

go.mod
module port-forward

go 1.15

require (
    github.com/aws/aws-sdk-go v1.36.0
    github.com/go-sql-driver/mysql v1.5.0
    golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
)
main.go
package main

import (
    "database/sql"
    "encoding/json"
    "errors"
    "flag"
    "fmt"
    "io"
    "io/ioutil"
    "net"
    "os"
    "os/exec"
    "path"
    "runtime"
    "strconv"

    "github.com/aws/aws-sdk-go/aws"
    "github.com/aws/aws-sdk-go/aws/session"
    "github.com/aws/aws-sdk-go/service/ssm"
    "github.com/go-sql-driver/mysql"
    "golang.org/x/crypto/ssh"
    "golang.org/x/crypto/ssh/knownhosts"
)

type config struct {
    instanceID string
    region     string
    user       string
    keyPath    string
    localPort  uint16
    dbHost     string
    dbPort     uint16
    dbUser     string
    dbPass     string
}

func main() {
    conf := &config{}

    var localPort, dbPort uint

    flags := flag.NewFlagSet("port-forward", flag.ContinueOnError)
    flags.StringVar(&conf.instanceID, "instance-id", "", "bastion server instance id")
    flags.StringVar(&conf.region, "region", "ap-northeast-1", "aws region")
    flags.StringVar(&conf.user, "ssh-user", "ec2-user", "ssh user for bastion server")
    flags.StringVar(&conf.keyPath, "key", "", "ssh key file path")
    flags.UintVar(&localPort, "local-port", 9090, "local port for port-fowarding")
    flags.StringVar(&conf.dbHost, "db-host", "", "database host")
    flags.UintVar(&dbPort, "db-port", 3306, "database port")
    flags.StringVar(&conf.dbUser, "db-user", "root", "database user")
    flags.StringVar(&conf.dbPass, "db-pass", "", "database password")
    if err := flags.Parse(os.Args[1:]); err != nil {
        os.Exit(2)
    }

    conf.localPort = uint16(localPort)
    conf.dbPort = uint16(dbPort)

    if err := run(conf); err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
    os.Exit(0)
}

func run(conf *config) error {
    sess, err := session.NewSession(&aws.Config{
        Region: aws.String(conf.region),
    })
    if err != nil {
        return err
    }

    svc := ssm.New(sess)

    proxyCmd, closeSession, err := openSession(svc, conf.instanceID)
    if err != nil {
        return err
    }
    defer closeSession()

    sshConfig, err := newSSHClientConfig(conf.user, conf.keyPath)
    if err != nil {
        return err
    }

    client, killProxyCmd, err := newSSHClientWithProxyCommand(conf.instanceID, 22, proxyCmd, sshConfig)
    if err != nil {
        return err
    }
    defer killProxyCmd()
    defer client.Close()

    done, err := portForward(conf.localPort, client, conf.dbHost, conf.dbPort)
    if err != nil {
        return err
    }
    defer done()

    if err := printDBList("localhost", conf.localPort, conf.dbUser, conf.dbPass); err != nil {
        return err
    }

    return nil
}

// openSession AWS Systems Manager Session Manager のセッションを開始し、
// session-manager-plugin を実行する *exec.Cmd とセッションを終了する関数を返す。
func openSession(svc *ssm.SSM, instanceID string) (*exec.Cmd, func() error, error) {
    in := &ssm.StartSessionInput{
        DocumentName: aws.String("AWS-StartSSHSession"),
        Parameters: map[string][]*string{
            "portNumber": {aws.String("22")},
        },
        Target: aws.String(instanceID),
    }
    out, err := svc.StartSession(in)
    if err != nil {
        return nil, nil, err
    }

    close := func() error {
        in := &ssm.TerminateSessionInput{
            SessionId: out.SessionId,
        }
        if _, err := svc.TerminateSession(in); err != nil {
            return err
        }
        return nil
    }

    cmd, err := sessionManagerPlugin(svc, in, out)
    if err != nil {
        defer close()
        return nil, nil, err
    }

    return cmd, close, nil
}

// sessionManagerPlugin session-manager-plugin を実行する *exec.Cmd を返す。
func sessionManagerPlugin(
    svc *ssm.SSM,
    in *ssm.StartSessionInput,
    out *ssm.StartSessionOutput,
) (*exec.Cmd, error) {
    command := "session-manager-plugin"
    if runtime.GOOS == "windows" {
        command += ".exe"
    }

    encodedIn, err := json.Marshal(in)
    if err != nil {
        return nil, err
    }
    encodedOut, err := json.Marshal(out)
    if err != nil {
        return nil, err
    }
    region := *svc.Config.Region
    profile := getAWSProfile()
    endpoint := svc.Endpoint

    cmd := exec.Command(command, string(encodedOut), region,
        "StartSession", profile, string(encodedIn), endpoint)

    return cmd, nil
}

// getAWSProfile 有効な AWS Profile を取得する。
func getAWSProfile() string {
    profile := os.Getenv("AWS_PROFILE")
    if profile != "" {
        return profile
    }

    enableSharedConfig, _ := strconv.ParseBool(os.Getenv("AWS_SDK_LOAD_CONFIG"))
    if enableSharedConfig {
        profile = os.Getenv("AWS_DEFAULT_PROFILE")
    }

    return profile
}

// newSSHClientConfig *ssh.ClientConfig を生成する。
func newSSHClientConfig(user string, keyPath string) (*ssh.ClientConfig, error) {
    key, err := ioutil.ReadFile(keyPath)
    if err != nil {
        return nil, err
    }

    signer, err := ssh.ParsePrivateKey(key)
    if err != nil {
        return nil, err
    }

    hostKeyCallback, err := newHostKeyCallback()
    if err != nil {
        return nil, err
    }

    return &ssh.ClientConfig{
        User: user,
        Auth: []ssh.AuthMethod{
            ssh.PublicKeys(signer),
        },
        HostKeyCallback: hostKeyCallback,
    }, nil
}

// newHostKeyCallback ~/.ssh/known_hosts を参照して
// ホストの公開鍵を確認する ssh.HostKeyCallback を返す。
func newHostKeyCallback() (ssh.HostKeyCallback, error) {
    home, err := os.UserHomeDir()
    if err != nil {
        return nil, err
    }

    knownHosts := path.Join(home, ".ssh", "known_hosts")

    cb, err := knownhosts.New(knownHosts)
    if err != nil {
        return nil, err
    }

    return func(hostname string, remote net.Addr, key ssh.PublicKey) error {
        // net.Pipe() から生成した net.Conn で ssh.Conn を作ると
        // remote.String() の値が "pipe" となり net.SplitHostPort() が失敗してしまう。
        // https://github.com/golang/crypto/blob/5f87f3452ae9/ssh/knownhosts/knownhosts.go#L336
        //
        // hostname には `${instance-id}:22` が入っているので
        // それを返す net.Addr に差し替えておく。
        if remote.String() == "pipe" {
            remote = &addrImpl{
                network: remote.Network(),
                addr:    hostname,
            }
        }

        err := cb(hostname, remote, key)

        var ke *knownhosts.KeyError
        if errors.As(err, &ke) {
            // known_hosts と一致しない場合はエラー
            if len(ke.Want) > 0 {
                return ke
            }

            f, err := os.OpenFile(knownHosts, os.O_WRONLY|os.O_APPEND, 0644)
            if err != nil {
                return err
            }
            defer f.Close()

            // 未知のホストの場合は known_hosts に追記する
            line := knownhosts.Line([]string{remote.String()}, key)
            fmt.Fprintln(f, line)

            return nil
        }

        return err
    }, nil
}

// addrImple net.Addr の実装。
type addrImpl struct {
    network string
    addr    string
}

func (s *addrImpl) Network() string {
    return s.network
}

func (s *addrImpl) String() string {
    return s.addr
}

// newSSHClientWithProxyCommand ProxyCommand を利用した *ssh.Client を返す。
func newSSHClientWithProxyCommand(
    host string,
    port uint16,
    proxyCmd *exec.Cmd,
    conf *ssh.ClientConfig,
) (*ssh.Client, func() error, error) {
    c, s := net.Pipe()

    proxyCmd.Stdin = s
    proxyCmd.Stdout = s
    proxyCmd.Stderr = os.Stderr

    if err := proxyCmd.Start(); err != nil {
        return nil, nil, err
    }

    done := func() error {
        return proxyCmd.Process.Kill()
    }

    addr := fmt.Sprintf("%s:%d", host, port)
    conn, chans, reqs, err := ssh.NewClientConn(c, addr, conf)
    if err != nil {
        defer done()
        return nil, nil, err
    }

    client := ssh.NewClient(conn, chans, reqs)

    return client, done, nil
}

// portForward ポートフォワードを行う。
func portForward(
    localPort uint16,
    sshClient *ssh.Client,
    remoteHost string,
    remotePort uint16,
) (func(), error) {
    listener, err := net.Listen("tcp", fmt.Sprintf(":%d", localPort))
    if err != nil {
        return nil, err
    }

    remoteAddr := fmt.Sprintf("%s:%d", remoteHost, remotePort)

    done := make(chan struct{})

    go func() {
        defer listener.Close()

        for {
            select {
            case <-done:
                return
            default:
            }

            localConn, err := listener.Accept()
            if err != nil {
                var ne net.Error
                if errors.As(err, &ne) && ne.Temporary() {
                    continue
                }
                fmt.Fprintln(os.Stderr, "accept failed: ", err)
                return
            }

            remoteConn, err := sshClient.Dial("tcp", remoteAddr)
            if err != nil {
                fmt.Fprintln(os.Stderr, "dial failed: ", err)
                return
            }

            go func() {
                defer localConn.Close()
                defer remoteConn.Close()
                if _, err := io.Copy(remoteConn, localConn); err != nil {
                    fmt.Fprintln(os.Stderr, "copy failed: ", err)
                }
            }()

            go func() {
                if _, err := io.Copy(localConn, remoteConn); err != nil {
                    fmt.Fprintln(os.Stderr, "copy failed: ", err)
                }
            }()
        }
    }()

    return func() {
        close(done)
    }, nil
}

// printDBList RDS に接続し DB 一覧を出力する。
func printDBList(host string, port uint16, user, password string) error {
    conf := mysql.NewConfig()
    conf.User = user
    conf.Passwd = password
    conf.Addr = fmt.Sprintf("%s:%d", host, port)
    conf.Net = "tcp"

    dsn := conf.FormatDSN()
    db, err := sql.Open("mysql", dsn)
    if err != nil {
        return err
    }

    res, err := db.Query("SHOW DATABASES")
    if err != nil {
        return err
    }
    defer res.Close()

    var database string
    for res.Next() {
        if err := res.Scan(&database); err != nil {
            return err
        }
        fmt.Println(database)
    }

    if err := res.Err(); err != nil {
        return err
    }

    return nil
}

前述の通り session-manager-plugin がインストールされている必要があります。

次のように踏み台サーバのインスタンス ID ・ RDS インスタンスのエンドポイント・秘密鍵のパスなどを与えて実行します。
(デフォルトでは RDS インスタンスの 3306 ポートがローカルの 9090 ポートにフォワーディングされます)

$ go run main.go -instance-id i-xxxxxx -key ~/.ssh/bastion.key -db-host xxxx.xxxx.ap-northeast-1.rds.amazonaws.com -db-pass xxxx
information_schema
mysql
performance_schema

プライベート VPC 内の RDS インスタンスに SHOW DATABASES を実行して得られた DB 一覧が出力されます。

コードの解説

要点だけを解説します。

メインの処理は run() 関数に実装されています。

func run(conf *config) error {
    sess, err := session.NewSession(&aws.Config{
        Region: aws.String(conf.region),
    })
    if err != nil {
        return err
    }

    svc := ssm.New(sess)

    proxyCmd, closeSession, err := openSession(svc, conf.instanceID)
    if err != nil {
        return err
    }
    defer closeSession()

    sshConfig, err := newSSHClientConfig(conf.user, conf.keyPath)
    if err != nil {
        return err
    }

    client, killProxyCmd, err := newSSHClientWithProxyCommand(conf.instanceID, 22, proxyCmd, sshConfig)
    if err != nil {
        return err
    }
    defer killProxyCmd()
    defer client.Close()

    done, err := portForward(conf.localPort, client, conf.dbHost, conf.dbPort)
    if err != nil {
        return err
    }
    defer done()

    if err := printDBList("localhost", conf.localPort, conf.dbUser, conf.dbPass); err != nil {
        return err
    }

    return nil
}

次のような流れになっています。

  1. openSession() で Session Manager のセッションを開始
  2. newSSHClientWithProxyCommand() で session-manager-plugin を ProxyCommand として使う SSH Client を生成
  3. portFoward() で RDS インスタンスのポートをローカルのポートにフォワーディング
  4. ローカルポートに対してクエリを実行

openSession()

// openSession AWS Systems Manager Session Manager のセッションを開始し、
// session-manager-plugin を実行する *exec.Cmd とセッションを終了する関数を返す。
func openSession(svc *ssm.SSM, instanceID string) (*exec.Cmd, func() error, error) {
    in := &ssm.StartSessionInput{
        DocumentName: aws.String("AWS-StartSSHSession"),
        Parameters: map[string][]*string{
            "portNumber": {aws.String("22")},
        },
        Target: aws.String(instanceID),
    }
    out, err := svc.StartSession(in)
    if err != nil {
        return nil, nil, err
    }

    close := func() error {
        in := &ssm.TerminateSessionInput{
            SessionId: out.SessionId,
        }
        if _, err := svc.TerminateSession(in); err != nil {
            return err
        }
        return nil
    }

    cmd, err := sessionManagerPlugin(svc, in, out)
    if err != nil {
        defer close()
        return nil, nil, err
    }

    return cmd, close, nil
}

SSM.StartSession() を叩いてセッションを開始します。
AWS CLI の実装と、SSH の ProxyCommand 設定での呼び出し方を参考に実装しています。

SSM.StartSession() の入出力を JSON エンコードしたものを session-manager-plugin に与える必要があるので、ここで session-manager-plugin を実行するための *exec.Cmd も生成してしまっています(実際に生成している箇所は sessionManagerPlugin())。

newSSHClientWithProxyCommand()

// newSSHClientWithProxyCommand ProxyCommand を利用した *ssh.Client を返す。
func newSSHClientWithProxyCommand(
    host string,
    port uint16,
    proxyCmd *exec.Cmd,
    conf *ssh.ClientConfig,
) (*ssh.Client, func() error, error) {
    c, s := net.Pipe()

    proxyCmd.Stdin = s
    proxyCmd.Stdout = s
    proxyCmd.Stderr = os.Stderr

    if err := proxyCmd.Start(); err != nil {
        return nil, nil, err
    }

    done := func() error {
        return proxyCmd.Process.Kill()
    }

    addr := fmt.Sprintf("%s:%d", host, port)
    conn, chans, reqs, err := ssh.NewClientConn(c, addr, conf)
    if err != nil {
        defer done()
        return nil, nil, err
    }

    client := ssh.NewClient(conn, chans, reqs)

    return client, done, nil
}

与えられた *exec.Cmd を ProxyCommand として使用する SSH Client を生成します。
net.Pipe() を使用してコマンドの入出力を SSH Client に結び付けるのがポイントです。

portFoward()

// portForward ポートフォワードを行う。
func portForward(
    localPort uint16,
    sshClient *ssh.Client,
    remoteHost string,
    remotePort uint16,
) (func(), error) {
    listener, err := net.Listen("tcp", fmt.Sprintf(":%d", localPort))
    if err != nil {
        return nil, err
    }

    remoteAddr := fmt.Sprintf("%s:%d", remoteHost, remotePort)

    done := make(chan struct{})

    go func() {
        defer listener.Close()

        for {
            select {
            case <-done:
                return
            default:
            }

            localConn, err := listener.Accept()
            if err != nil {
                var ne net.Error
                if errors.As(err, &ne) && ne.Temporary() {
                    continue
                }
                fmt.Fprintln(os.Stderr, "accept failed: ", err)
                return
            }

            remoteConn, err := sshClient.Dial("tcp", remoteAddr)
            if err != nil {
                fmt.Fprintln(os.Stderr, "dial failed: ", err)
                return
            }

            go func() {
                defer localConn.Close()
                defer remoteConn.Close()
                if _, err := io.Copy(remoteConn, localConn); err != nil {
                    fmt.Fprintln(os.Stderr, "copy failed: ", err)
                }
            }()

            go func() {
                if _, err := io.Copy(localConn, remoteConn); err != nil {
                    fmt.Fprintln(os.Stderr, "copy failed: ", err)
                }
            }()
        }
    }()

    return func() {
        close(done)
    }, nil
}

ポートフォワードを行います。
Listener.Accept() で得られたローカルポートの net.Conn と SSH Client から RDS インスタンスに Dial() して得られた net.Conn とを goroutine 内で相互に io.Copy() することでポートフォワードを実現できます。

無限ループ内で Accept(), Dial() することで複数のコネクションを扱うことが可能です。

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

UbuntuにAWS IoT Greengrassをインストールする

はじめに

会社の業務でAWS IoT Greengrassについて勉強したので、今回から数回に分けてUbuntu搭載デバイスとAWS IoTを利用したアプリケーションを紹介していきたいと思います。
初回はクラウド上でのGreengrass Groupの作成と、エッジデバイスへのGreengrass Coreのインストール方法を紹介します。

Azure IoT Edgeを使った記事もあるので興味のある方は是非ご覧ください。

環境

動作確認済デバイス(OS)

  • e-RT3 Plus F3RP70-2L1(Ubuntu 18.04 32bit)
    横河電機のエッジコントローラです。AWS IoT Greengrassの認定デバイス2に登録されています(e-RT3のページはこちら)。

  • Raspberry Pi 4 Model B (Ubuntu Server 20.04 32bit)

これらのデバイスでは armhf アーキテクチャのパッケージが動作します。
また、Windows 10 搭載のPCでデバイスを操作しています。

AWS IoT Greengrassとは

AWS IoT GreengrassとはAWSが提供しているエッジコンピューティングのためのソフトウェアです。
Greengrassをエッジデバイスにインストールすることにより、クラウドとの接続やクラウドからのアプリケーションのデプロイなどを容易に行うことができます。
また、クラウドの一部の機能をエッジデバイスに拡張することにより、データソースに近い場所でのデータ収集や分析、ローカルイベントに対するアクション、ローカルデバイス同士の通信などを行うことができます。
詳しくはAWS IoT Greengrassの公式サイトをご覧ください。

準備

AWSアカウントの作成

AWSのアカウントを所持していない場合は作成します。
https://aws.amazon.com/jp/
制限付きの無料利用枠もあります。
今回使用するAWS IoTの無料利用枠はここで確認できます。

WinSCPのインストール

PCからデバイスにファイルを転送するために、PCにWinSCPをインストールします。
インストール方法や使い方については公式サイトをご覧ください。
https://winscp.net/eng/index.php

Python3.8のインストール

※この設定はe-RT3を使用している場合のみ必要です。
※e-RT3の場合、一般ユーザーでsudoコマンドを実行するにはsudoers設定が必要です。

GreengrassのLambdaで使用するPython3.8をインストールします。

sudo apt update
sudo apt install python3.8

インストールの成功を確認します。

username@ubuntu:~$ python3.8 --version
Python 3.8.0

Java8のインストール

Greengrassのストリームマネージャーで使用するJava8をインストールします。

sudo apt update
sudo apt install openjdk-8-jdk

インストールしたJavaにリンクを張ります。

sudo ln /etc/alternatives/java /usr/bin/java8

インストールの成功を確認します。

username@ubuntu:~$ java8 -version
openjdk version "1.8.0_275"
OpenJDK Runtime Environment (build 1.8.0_275-8u275-b01-0ubuntu1~18.04-b01)
OpenJDK Client VM (build 25.275-b01, mixed mode)

cgroupの有効化

※この設定はRaspberry Piを使用している場合のみ必要です。

コンテナでLambdaを実行するためにcgroupを有効にします。

/boot/firmware/cmdline.txtを開きます。

sudo vi /boot/firmware/cmdline.txt

既存の行の末尾に以下の値を追加します。

cgroup_enable=memory cgroup_memory=1

デバイスを再起動します。

sudo reboot

Greengrass Groupの作成

AWSの公式ガイドの手順3に従って、AWSマネジメントコンソールでGreengrass Groupを作成します。

  1. 右上のメニューで東京リージョンを選択します。
    gg0.png

  2. 左上の「サービス」メニューを開いて検索窓に「iot greengrass」と入力し、「IoT Greengrass」をクリックします。
    gg1.png

  3. 左側のナビゲーションペインから「Greengrass」→「イントロダクション」と進み、グループの作成をクリックします。
    gg2.png

  4. 「デフォルト作成を使用」をクリックします。

  5. グループに名前を付けて「次へ」をクリックします。今回は「eRT3Group」とします。

  6. Greengrass Coreの名前を指定します。デフォルトのまま「次へ」をクリックします。

  7. 確認画面が表示されます。確認して「グループとCoreの作成」をクリックします。

8.「これらのリソースはtar.gzとしてダウンロードしてください」をクリックして、デバイスをクラウドに接続するためのセキュリティリソースをダウンロードします。
このファイルは後からダウンロードすることができないので、必ずここでダウンロードしておきましょう。
ダウンロードできたら「完了」をクリックして設定を終了します。

Greengrass Coreのデバイスへのインストール

デバイスにGreengrass Coreソフトウェアをインストールして起動します。
Greengrass Coreソフトウェアのインストール方法は複数ありますが、ここではAPTリポジトリからインストールします4
※APTリポジトリからのインストールではOTA更新がサポートされていません。OTA更新を利用したい方は他の方法でインストールしてください。

セキュリティリソースとルートCA証明書のセットアップ

デバイスをクラウドに接続するために必要なセキュリティリソースとルートCA証明書をセットアップします。

セキュリティリソースのインストール

  1. WinSCPを起動してデバイスと接続し、ユーザーのホームディレクトリにGreengrass Groupの作成の手順8でダウンロードしたセキュリティリソース(tar.gzファイル)を置きます。
    gg8-2.png

  2. 以下のコマンドを実行してセキュリティリソースをインストールします。
    <hash>の部分はセキュリティリソースのファイル名に合わせて置き換えてください。

    sudo mkdir -p /greengrass
    sudo tar -xzvf ~/<hash>-setup.tar.gz -C /greengrass
    

ルートCA証明書のダウンロード

  1. /greengrass/certsへ移動します。

    cd /greengrass/certs/
    
  2. root.ca.pemという名前でAmazon Root CA 1証明書をダウンロードします。

    sudo wget -O root.ca.pem https://www.amazontrust.com/repository/AmazonRootCA1.pem
    

    ダウンロードしたroot.ca.pemが空でない(正しくダウンロードされた)ことを確認します。ファイルが空の場合はもう一度ダウンロードを試してみてください。

    cat root.ca.pem
    

Greengrass Coreソフトウェアのインストール

  1. Greengrassのシステムアカウントを作成します。

    sudo adduser --system ggc_user
    sudo addgroup --system ggc_group
    
  2. AWS IoT Greengrass キーリングパッケージをインストールして、リポジトリを追加します。

    cd ~
    sudo wget -O aws-iot-greengrass-keyring.deb https://d1onfpft10uf5o.cloudfront.net/greengrass-apt/downloads/aws-iot-greengrass-keyring.deb
    sudo dpkg -i aws-iot-greengrass-keyring.deb
    
    username@ubuntu:~$ echo "deb https://dnw9lb6lzp2d8.cloudfront.net stable main" | sudo tee /etc/apt/sources.list.d/greengrass.list
    deb https://dnw9lb6lzp2d8.cloudfront.net stable main
    
  3. パッケージのリストを更新し、Greengrass Coreソフトウェアをインストールします。

    sudo apt update
    sudo apt install aws-iot-greengrass-core
    
  4. Greengrassデーモンを開始します。

    sudo systemctl start greengrass.service
    

    以下のコマンドを実行し、表示されたActiveの状態がactive(running)であればデーモンは正常に動作しています。

    username@ubuntu:~$ systemctl status greengrass.service
    * greengrass.service - Greengrass Daemon
    Loaded: loaded (/lib/systemd/system/greengrass.service; disabled; vendor preset: enabled)
    Active: active (running) since Fri 2020-11-06 06:31:07 UTC; 14min ago
    Process: 2159 ExecStart=/greengrass/ggc/core/greengrassd start (code=exited, status=0/SUCCESS)
    Main PID: 2163 (5)
    Tasks: 10 (limit: 2366)
    CGroup: /system.slice/greengrass.service
    

    デバイス起動時に自動的に起動するには以下のコマンドを実行してください。

    sudo systemctl enable greengrass.service
    

※デバイスがproxy環境下にある場合は追加でproxy設定が必要になります。

動作確認

空のデプロイを行ってデバイスがクラウドと接続できるか確認します。

  1. 左側のメニューから「グループ」へ進み、作成したグループをクリックします。
    gg11-a.png

  2. 「アクション」メニューから「デプロイ」をクリックし、デプロイを開始します。
    gg9.png

  3. 「自動検出」をクリックします。
    gg28.png

  4. デプロイが完了するまで数分間待ちます。デプロイが正常に完了すれば成功です。
    gg10.png

まとめ

クラウド上でのGreengrass Groupの作成と、デバイスへのGreengrass Coreのインストールを行いました。
次回はLambdaの作成とデプロイを行う予定ですのでお楽しみに!

補足

proxy設定

デバイスがproxy環境下にある場合はproxy設定が必要になります。
環境により設定は異なりますが、参考までに今回私が行った設定を紹介します。

環境変数の設定

こちらの記事をご覧ください。

Greengrass Coreの設定

AWSの公式ドキュメント5に従ってGreengrass Coreの設定を行います。

  1. Greengrassが起動中の場合は停止します。

    sudo systemctl stop greengrass.service
    
  2. /greengrass/config/config.jsonに書き込み権限を追加して開きます。

    sudo chmod +w /greengrass/config/config.json
    sudo vi /greengrass/config/config.json
    

    coreThingオブジェクト内にiotMqttPortオブジェクトとnetworkProxyオブジェクトを追加します。
    追加した後のファイルは以下のようになります。

    /greengrass/config/config.json
    {
    "coreThing" : {
        "caPath" : "root.ca.pem",
        "certPath" : "3283c6f04d.cert.pem",
        "keyPath" : "3283c6f04d.private.key",
        "thingArn" : "arn:aws:iot:ap-northeast-1:xxxxxxxxxxxx:thing/eRT3Group_Core",
        "iotHost" : "xxxxxxxxxxxx-ats.iot.ap-northeast-1.amazonaws.com",
        "iotMqttPort" : 443,
        "ggHost" : "greengrass-ats.iot.ap-northeast-1.amazonaws.com",
        "keepAlive" : 600,
        "networkProxy":{
            "proxy":{
                "url" : "http://username:password@example.com:port/"
            }
        }
    },
    ...
    
  3. ファイルから書き込み権限を削除し、Greengrassを再度起動します。

    sudo chmod -w /greengrass/config/config.json
    sudo systemctl start greengrass.service
    

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AWSサポートプラン4種比較

具体的なサポート内容は 公式情報を参照

ベーシック 開発者 ビジネス エンタープライズ
対応期間 - 月~金,09:00~18:00 24時間年中無休 24時間年中無休
応答速度 - 12h以内 1h以内 15min以内
コスト 無料 29USD~ 100USD~ 15000USD~
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

個人的によく使うAWS CLIコマンド

概要

個人的によく使うAWS CLIのコマンド集.
yamlが好きなので json2yaml が頻出しますが,そこは従うなり無視するなりご自由に...

EC2インスタンス情報のサマリ

INSTANCE_ID=''
AWS_REGION='ap-northeast-1'

aws ec2 describe-instances \
    --filters Name=instance-id,Values=${INSTANCE_ID} \
    --region ${AWS_REGION} \
    --query 'Reservations[].Instances[].{InstanceId:InstanceId,PrivateIpAddress:PrivateIpAddress,PrivateDnsName:PrivateDnsName,PublicIpAddress:PublicIpAddress,PublicDnsName:PublicDnsName,Tags:Tags[]}[0]' \
    --output json \
    | json2yaml

Output

Tags:
- Value: ******
  Key: DeviceID
- Value: ******
  Key: Name
- Value: ******
  Key: PIC
- Value: ******
  Key: ExpireDate
- Value: ******
  Key: CreatedDate
InstanceId: ******
PublicDnsName: ******
PrivateDnsName: ******
PublicIpAddress: ******
PrivateIpAddress: ******

インターネットゲートウェイ情報

INTERNET_GATEWAY_IDS=''

aws ec2 describe-internet-gateways \
    --internet-gateway-ids ${INTERNET_GATEWAY_IDS} \
    | json2yaml

Output

InternetGateways:
- OwnerId: ******
  Tags:
  - Value: ******
    Key: Name
  - Value: ******
    Key: CreatedDate
  - Value: ******
    Key: ExpireDate
  Attachments:
  - State: available
    VpcId: ******
  InternetGatewayId: igw-******

ルートテーブル情報

ROUTE_TABLE_IDS='rtb-******'

aws ec2 describe-route-tables \
    --route-table-ids ${ROUTE_TABLE_IDS} \
    | json2yaml

Output

RouteTables:
- Associations:
  - AssociationState:
      State: associated
    RouteTableAssociationId: ******
    Main: true
    RouteTableId: rtb-******
  RouteTableId: rtb-******
  VpcId: vpc-******
  PropagatingVgws:
  - GatewayId: vgw-******
  Tags:
  - Value: ******
    Key: ExpireDate
  - Value: ******
    Key: Name
  - Value: ******
    Key: CreatedDate
  Routes:
  - GatewayId: local
    DestinationCidrBlock: ******/**
    State: active
    Origin: CreateRouteTable
  - GatewayId: vgw-******
    DestinationCidrBlock: ******/**
    State: active
    Origin: EnableVgwRoutePropagation
  OwnerId: ******

AMI情報

IMAGE_IDS=''
AWS_REGION='ap-northeast-1'

aws ec2 describe-images \
    --region ${AWS_REGION} \
    --image-ids ${IMAGE_IDS} \
    | json2yaml

Output

Images:
- ProductCodes:
  - ProductCodeId: ******
    ProductCodeType: ******
  Description: ******
  Tags:
  - Value: ******
    Key: Name
  VirtualizationType: hvm
  Hypervisor: xen
  EnaSupport: true
  SriovNetSupport: simple
  ImageId: ******
  State: available
  BlockDeviceMappings:
  - DeviceName: /dev/sda1
    Ebs:
      SnapshotId: ******
      DeleteOnTermination: false
      VolumeType: gp2
      VolumeSize: 8
      Encrypted: true
  Architecture: x86_64
  ImageLocation: ******/******
  RootDeviceType: ebs
  OwnerId: ******
  RootDeviceName: /dev/sda1
  CreationDate: '2020-01-11T00:00:00.000Z'
  Public: false
  ImageType: machine
  Name: ******

セキュリティグループ情報

GROUP_IDS='sg-****** sg-******' のように複数指定可能.

GROUP_IDS=''

aws ec2 describe-security-groups \
    --group-ids ${GROUP_IDS} \
    | json2yaml

Output

SecurityGroups:
- IpPermissionsEgress:
  - IpProtocol: '-1'
    PrefixListIds: []
    IpRanges:
    - CidrIp: 0.0.0.0/0
    UserIdGroupPairs: []
    Ipv6Ranges: []
  Description: test-sg-1
  IpPermissions:
  - PrefixListIds: []
    FromPort: 80
    IpRanges:
    - CidrIp: ***.***.***.***/32
    ToPort: 80
    IpProtocol: tcp
    UserIdGroupPairs: []
    Ipv6Ranges: []
  GroupName: test-sg-1
  VpcId: ******
  OwnerId: ******
  GroupId: ******
- IpPermissionsEgress:
  - IpProtocol: '-1'
    PrefixListIds: []
    IpRanges:
    - CidrIp: 0.0.0.0/0
    UserIdGroupPairs: []
    Ipv6Ranges: []
  Description: test-sg-2
  IpPermissions:
  - PrefixListIds: []
    FromPort: 80
    IpRanges:
    - CidrIp: ***.***.***.***/32
    ToPort: 80
    IpProtocol: tcp
    UserIdGroupPairs: []
    Ipv6Ranges: []
  GroupName: test-sg-2
  VpcId: ******
  OwnerId: ******
  GroupId: ******

S3バケット一覧

aws s3api list-buckets \
  --query "Buckets[].Name"

Output

[
    "test-bucket-1", 
    "test-bucket-2", 
    "test-bucket-3", 
    "test-bucket-4", 
    "test-bucket-5"
]

S3バケットのタグ情報

BUCKET_NAME=''

aws s3api get-bucket-tagging \
  --bucket ${BUCKET_NAME} \
  | json2yaml

Output

TagSet:
- Value: '20210101'
  Key: Expire
- Value: '20200101'
  Key: Created
- Value: Production
  Key: Environment
- Value: test-bucket-1
  Key: Name

S3 ls

便利(?)なオプション.結果についてはOutput参照

  • --human
    • ファイルサイズに単位を付けて表示
  • --sum
    • オブジェクト数,ファイルサイズ合計を表示
  • --recursive
    • 再帰的表示
aws s3 ls s3://test-bucket-1/ \
  --recursive \
  --human \
  --sum

Output

aws s3 ls s3://test-bucket-1/ \
  --recursive
2010-01-11 18:16:16         88 AWSLogs/AccessLog
2010-01-11 18:20:08        646 AWSLogs/elasticloadbalancing/.../test-web.log.gz
aws s3 ls s3://test-bucket-1/ \
  --recursive \
  --sum    
2010-01-11 18:16:16         88 AWSLogs/AccessLog
2010-01-11 18:20:08        646 AWSLogs/elasticloadbalancing/.../test-web.log.gz

Total Objects: 2
   Total Size: 734
aws s3 ls s3://test-bucket-1/ \
  --recursive \
  --human \
  --sum
2010-01-11 18:16:16   88 Bytes AWSLogs/AccessLog
2010-01-11 18:20:08  646 Bytes AWSLogs/elasticloadbalancing/.../test-web.log.gz

Total Objects: 2
   Total Size: 734 Bytes

備考

随時加筆予定...

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Boto3でAWSのリソースを取得する時に作成したリソースだけ表示する

前提

AWSのリソースをBoto3で取得する時に、一部のリソースだと作った覚えのないリソースが含まれてしまうことがある。
これらはおそらくAWSが自動で作成したり、デフォルトで用意されているものだろう。
私の場合、スナップショット一覧を取得しようとしたら、AWSコンソール上では見たことないし、作った覚えがないリソースがあった。
以下がソースである。

client = self.session.client('ec2')
snapshots = client.describe_snapshots()

対処方法

自身が作成したものだけを表示するためにはOwnerIdsを指定してあげれば良い。

自分が作ったリソースを表示

client = self.session.client('ec2')
snapshots = client.describe_snapshots(OwnerIds=['self'])

ユーザIDを指定

client = self.session.client('ec2')
snapshots = client.describe_snapshots(OwnerIds=['xxxxxxxxxxxx'])

※ xxxxxxxxxxxx にユーザIDを指定

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[AWS SAM] Lambda Layerの定義(nodejs)

目的

前回記事cfn-response モジュールを使用したが、
インライン実装にしか対応しておらず不便なためLayerとして実装する

ファイル構成

lambda-layer/
 ├─ layers/
 │    └─ cfn-response-layer/
 │              └─ nodejs/
 │                   ├─ node_modules/  # [npm i cfn-response] で生成
 │                   ├─ cfn-response/
 │                   │       ├─ package.json # [npm init] で生成
 │                   │       └─ index.js  # Layerとして提供する機能
 │                   │
 │                   ├─ package.json
 │                   └─ package-lock.json  # [npm i cfn-response] で生成
 
 └─ template.yaml # SAMテンプレート

nodejsフォルダは名前変更不可
index.jsファイルは名前変更不可ではないが、別名とした場合はnpm initした際にファイル名の指定が必要

SAMテンプレート

template.yaml
  CfnResponse:
    Type: AWS::Serverless::LayerVersion
    Properties:
      LayerName: !Sub ${RootStackName}_cfn-response
      ContentUri: layers/cfn-response-layer
      CompatibleRuntimes:
        - nodejs12.x
      RetentionPolicy: Delete

Layerとして提供する機能はnodejsフォルダに入れることが決められているので、
ContentUriには、その1個手前までのパスを指定

cfn-response モジュール実装

index.js

index.jsにcfn-response モジュールの機能を実装する
githubで公開されているcfn-response モジュールのコードを基本コピペする
SUCCESS, FAILEDの定義値と、send関数を提供する

index.js
const https = require('https');
const url = require('url');

module.exports = {
  SUCCESS: 'SUCCESS',
  FAILED: 'FAILED',
  send(
    event,
    context,
    responseStatus,
    responseData,
    physicalResourceId,
    noEcho,
  ) {
    return new Promise((resolve, reject) => {
      const responseBody = JSON.stringify({
        Status: responseStatus,
        Reason: `See the details in CloudWatch Log Stream: ${context.logStreamName}`,
        PhysicalResourceId: physicalResourceId || context.logStreamName,
        StackId: event.StackId,
        RequestId: event.RequestId,
        LogicalResourceId: event.LogicalResourceId,
        NoEcho: noEcho || false,
        Data: responseData,
      });

      console.log('Response body:\n', responseBody);

      const parsedUrl = url.parse(event.ResponseURL);
      const options = {
        hostname: parsedUrl.hostname,
        port: 443,
        path: parsedUrl.path,
        method: 'PUT',
        headers: {
          'content-type': '',
          'content-length': responseBody.length,
        },
      };

      const request = https.request(options, response => {
        console.log(`Status code: ${response.statusCode}`);
        console.log(`Status message: ${response.statusMessage}`);
        resolve(context.done());
      });

      request.on('error', error => {
        console.log(`send(..) failed executing https.request(..): ${error}`);
        reject(context.done(error));
      });

      request.write(responseBody);
      request.end();
    });
  },
};

nodejs/cfn-response/package.json

nodejs/cfn-response/以下でnpm initを実行する
全てデフォルト値で回答すると以下のようなpackage.jsonが生成される

nodejs/cfn-response/package.json
{
  "name": "cfn-response",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}

nodejs/package.json

package.jsonは内容{}のみで作成
npm install <パッケージ名>を実行すると追記される

node_modulesおよびpackage-lock.json

nodejsフォルダ以下でnpm i cfn-responseを実行すると生成される
index.js内でhttpsurlパッケージを使用しているが、これらはnodejsの標準モジュールのためインストールは不要

Layerの使用

SAMテンプレート

  ApplyNotificationFunction:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: !Sub ${RootStackName}-ApplyNotificationFunction
      CodeUri: handlers
      Role: !GetAtt 'ApplyNotificationFunctionRole.Arn'
      Layers:
        - !Ref CfnResponse  # 先ほどSAMテンプレートで定義したLayerを参照

コード

const cfnResponse = require('cfn-response');

exports.handler = async (event, context) => {
  try {
    if (event.RequestType !== 'Create') {
      await cfnResponse.send(event, context, cfnResponse.SUCCESS);
      return;
    }
    ...

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

CloudFormationでEC2を構築した時にEBSにTagsを付与できない

かゆいところに手が届かない。

BlockDeviceMappings:
  - DeviceName: /dev/sda1
    Ebs:
      VolumeSize: 50
    Tags # 不可能
      -  Key: "keyname1"
         Value: "value1"

公式が情報をナレッジセンターに出しているが、余計な情報が多すぎる、ここだけでいい。

UserData:
  Fn::Base64: !Sub |
     #!/bin/bash
     AWS_AVAIL_ZONE=$(curl http://169.254.169.254/latest/meta-data/placement/availability-zone)
     AWS_REGION="`echo \"$AWS_AVAIL_ZONE\" | sed 's/[a-z]$//'`"
     AWS_INSTANCE_ID=$(curl http://169.254.169.254/latest/meta-data/instance-id)
     ROOT_VOLUME_IDS=$(aws ec2 describe-instances --region $AWS_REGION --instance-id $AWS_INSTANCE_ID --output text --query Reservations[0].Instances[0].BlockDeviceMappings[0].Ebs.VolumeId)
     aws ec2 create-tags --resources $ROOT_VOLUME_IDS --region $AWS_REGION --tags Key=Name,Value={つけたい名前}

aws ec2 create-tagsするためにはIAMロールとポリシーが必要だから、IAMを書いているとここまでする必要あるのか?ともやもやする。
ちなみに新規にVolumeを作るときは普通に対応している。

Type: AWS::EC2::Volume
Properties: 
  AutoEnableIO: Boolean
  AvailabilityZone: String
  Encrypted: Boolean
  Iops: Integer
  KmsKeyId: String
  Size: Integer
  SnapshotId: String
  Tags: 
    - Tag # だよね~
  VolumeType: String

CloudFormationで必要なだけインスタンス立ててから、LambdaでEC2のTagから拾ってきて一括付与でもいいんだけど、めんどくさ過ぎるからそもそも対応してほしい。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

CloudFormationに日本語コメントを含めるとエラーになる場合の解決方法

前提条件

  • Windowsで*.ymlに日本語コメントを含めてAWS CLIを叩いた場合のみ起こる
    • マネコンから*.ymlをアップロードした場合は不明
  • AWS CLIをインストーラからインストールしている場合のみ起こる
    • 結論から言うとインストーラからインストールしているAWS CLIをアンインストールしてpipでgithubからインストールすればいい

テンプレートに日本語コメントを含めるとエラーになる

# これは日本語だ
AWSTemplateFormatVersion: '2010-09-09'
Parameters:
  Parameter:
    Type: Number

Resources:
  # 日本語だ、これは
  Lambda:
    Type: 'AWS::Lambda::Function'

こういうテンプレートファイルをaws cloudformation create-stack --template-body file://hoge.yml --stack-name fooと実行すると

Error parsing parameter '--template-body': Unable to load paramfile (hoge.yml), text contents could not be decoded.  If this is a binary file, please use the fileb:// prefix instead of the file:// prefix.

いったエラーが出ることがある。fileb://とあるが、もちろんテンプレートはバイナリではない。AWS CLI実行時に--debugをつけるとこのようなエラーが見える。

UnicodeDecodeError: 'cp932' codec can't decode byte 0xef in position 89: illegal multibyte sequence

文字コード関連のエラーらしい

VS Codeで記述したので、テンプレートファイルはUTF8である。根本的解決にならないが、テンプレートファイルをShift_JISに変換してAWS CLIを実行すれば問題ない。ただ、JISのままだと、GithubにPushすると文字化けする等、いろいろと面倒。というかつい先日にMicrosoftからUnicodeを使ってくれとお触れが出たばっかりである。

AWS CLIが食っている文字コードをUTF8にする

調べたところ、AWS CLIは後ろでboto(Python)が動いているらしい。

C:\> aws --version
aws-cli/2.0.3 Python/3.7.5 Windows/10 botocore/2.0.0dev7

管理者権限を持つCMDなりPowershellで環境変数を付加してやる。参考にしたのはこの記事

setx /m PYTHONUTF8 1

何も変わらん( ^ω^)…

'cp932' codec can't decode byte 0xef in position 89: illegal multibyte sequence

CLIが使っているPythonは環境変数を読んでないらしい

インストーラから入れたAWS CLIをアンインストールする。
参考にした記事はこの記事、要するにPythonが環境変数を読んでないなら、環境変数を読ませたいPythonのパッケージマネージャであるpipから、botoとAWS CLIをインストールしてやればいい。

pip install https://github.com/boto/botocore/archive/v2.tar.gz
pip install https://github.com/aws/aws-cli/archive/v2.tar.gz

解決した

aws cloudformation create-stack --template-body file://lambda.yml --stack-name foo 

{
    "StackId": "arn:aws:cloudformation:ap-northeast-1:111111111111:stack/foo/00000000-1111-1111-1111-111111111111"
}

環境構築でハマるのもストレスマッハだけど、文字コード関連もそれに準ずるくらいイラつく。

参考記事

WindowsでCP932(Shift-JIS)エンコード以外のファイルを開くのに苦労した話
https://qiita.com/Yuu94/items/9ffdfcb2c26d6b33792e
Windows 上の Python で UTF-8 をデフォルトにする
https://qiita.com/methane/items/9a19ddf615089b071e71
AWS CLI v2をpipからインストールしてみた
https://dev.classmethod.jp/cloud/aws/install-aws-cli-v2-from-sourcecode/

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

CloudFormationに日本語コメントを含みたい

前提条件

  • Windowsで*.ymlに日本語コメントを含めてAWS CLIを叩いた場合のみ起こる
    • マネコンから*.ymlをアップロードした場合は不明
  • AWS CLIをインストーラからインストールしている場合のみ起こる
    • 結論から言うとインストーラからインストールしているAWS CLIをアンインストールしてpipでgithubからインストールすればいい

テンプレートに日本語コメントを含めるとエラーになる

# これは日本語だ
AWSTemplateFormatVersion: '2010-09-09'
Parameters:
  Parameter:
    Type: Number

Resources:
  # 日本語だ、これは
  Lambda:
    Type: 'AWS::Lambda::Function'

こういうテンプレートファイルをaws cloudformation create-stack --template-body file://hoge.yml --stack-name fooと実行すると

Error parsing parameter '--template-body': Unable to load paramfile (hoge.yml), text contents could not be decoded.  If this is a binary file, please use the fileb:// prefix instead of the file:// prefix.

いったエラーが出ることがある。fileb://とあるが、もちろんテンプレートはバイナリではない。AWS CLI実行時に--debugをつけるとこのようなエラーが見える。

UnicodeDecodeError: 'cp932' codec can't decode byte 0xef in position 89: illegal multibyte sequence

文字コード関連のエラーらしい

VS Codeで記述したので、テンプレートファイルはUTF8である。根本的解決にならないが、テンプレートファイルをShift_JISに変換してAWS CLIを実行すれば問題ない。ただ、JISのままだと、GithubにPushすると文字化けする等、いろいろと面倒。というかつい先日にMicrosoftからUnicodeを使ってくれとお触れが出たばっかりである。

AWS CLIが食っている文字コードをUTF8にする

調べたところ、AWS CLIは後ろでboto(Python)が動いているらしい。

C:\> aws --version
aws-cli/2.0.3 Python/3.7.5 Windows/10 botocore/2.0.0dev7

管理者権限を持つCMDなりPowershellで環境変数を付加してやる。参考にしたのはこの記事

setx /m PYTHONUTF8 1

何も変わらん( ^ω^)…

'cp932' codec can't decode byte 0xef in position 89: illegal multibyte sequence

CLIが使っているPythonは環境変数を読んでないらしい

インストーラから入れたAWS CLIをアンインストールする。
参考にした記事はこの記事、要するにPythonが環境変数を読んでないなら、環境変数を読ませたいPythonのパッケージマネージャであるpipから、botoとAWS CLIをインストールしてやればいい。

pip install https://github.com/boto/botocore/archive/v2.tar.gz
pip install https://github.com/aws/aws-cli/archive/v2.tar.gz

解決した

aws cloudformation create-stack --template-body file://lambda.yml --stack-name foo 

{
    "StackId": "arn:aws:cloudformation:ap-northeast-1:111111111111:stack/foo/00000000-1111-1111-1111-111111111111"
}

環境構築でハマるのもストレスマッハだけど、文字コード関連もそれに準ずるくらいイラつく。

参考記事

WindowsでCP932(Shift-JIS)エンコード以外のファイルを開くのに苦労した話
https://qiita.com/Yuu94/items/9ffdfcb2c26d6b33792e
Windows 上の Python で UTF-8 をデフォルトにする
https://qiita.com/methane/items/9a19ddf615089b071e71
AWS CLI v2をpipからインストールしてみた
https://dev.classmethod.jp/cloud/aws/install-aws-cli-v2-from-sourcecode/

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Amazon SageMakerとAzure MLにおける機械学習モデルのサービング技術比較(前編)

初版: 2020年11月18日
著者: 橋本恭佑、柿田将幸, 株式会社 日立製作所

はじめに

機械学習技術のビジネスへの活用ニーズが高まり、機械学習モデルの開発や運用のライフサイクルを支援するフレームワークに注目が集まっています。

パブリッククラウドでは上述のフレームワークを公開しており、フレームワーク利用者は機械学習モデルを迅速に作成し自身のシステムに組み込んで公開することができます。

機械学習モデルをシステムに組み込み、サービスとして公開する一連の技術はサービング技術と呼ばれており、
システム構築と運用を担うSEにとって重要な技術です。

本連載ではAI案件に対応するSEを対象として、Amazon SageMakerとAzure MLを例に、パブリッククラウドが提供するフレームワークとサービング技術を概観し、2つのクラウドベンダにどのような違いや特徴があるかを説明します。
なお、本連載に記載のAmazon SageMakerまたはAzure MLの情報は2020年9月末日現在のものであり、今後のアップデート等によって内容が変わることがあります。

投稿一覧

機械学習システムのライフサイクルとサービング技術

機械学習案件ではデータサイエンティストとSEが連携して運用サイクルを回します。

パブリッククラウドベンダの機械学習サービスでは、図1のように機械学習の継続的な運用サイクルを実現する一連の機能群を提供しています。

本連載ではAmazon SageMakerとAzure MLの推論システム構築機能の違いを解説します。

図1: 機械学習の継続的な運用サイクル
図1: 機械学習の継続的な運用サイクル

出典: Machine Learning with Amazon SageMaker(https://docs.aws.amazon.com/sagemaker/latest/dg/how-it-works-mlconcepts.html)

パブリッククラウドで実現する推論システムの種別

推論システムは利用用途により図2のようにストリーム型とバッチ型の2種類に大別されます。
特にストリーム型はリクエストの到着間隔がランダムであり、バッチ型と比較して
事前の性能設計が困難であるため、必要に応じた計算リソースの増減が可能なパブリッククラウドにより実現しやすいといえます。
そこで本連載では、ストリーム型について紹介します。

図2: 推論システムの種別
図2: 推論システムの種別

ストリーム型推論システムの概要と構築の勘所

パブリッククラウドにおけるサービングでは、ゲートウェイ、前処理、モデル、後処理、およびそれらの実行に必要なライブラリ群を、まとめて「ランタイム」としてデプロイします。
以後では特に初学者向けに、パブリッククラウド上でモデルを学習させてランタイムを作成する場合の構築の勘所を、アプリ層と基盤層に分けて説明します。

図3: ストリーム型推論システムの概要と構築の勘所

ストリーム型推論システム構築に向けたパブリッククラウドサービスの機能比較: アプリ層

表1から、Amazon SageMakerとAzure MLの両者で、前処理や後処理の実装方法が大きく異なることがわかります。
モデル作成時は、各サービスであらかじめ用意されたバージョンのOSSを利用します。

表1: パブリッククラウドサービスの機能比較: アプリ層

カテゴリ 項目 Amazon SageMaker Azure ML
ゲートウェイ 送信データの形式 MIME Type(テキスト、画像、音声) MIME Type(テキスト、画像、音声)
受信データの形式 MIME Type(テキスト、画像、音声) MIME Type(テキスト、画像、音声)
通信プロトコル HTTPのみ HTTPのみ
認証 IAMベースのアカウント認証 Azureロールベースのアカウント認証
前処理・後処理 実装方法(後編で実機検証) 前処理と後処理を異なる関数に分けて実装 前処理・後処理も同じ関数内に実装
モデル 学習や推論に利用するOSS コンテナイメージに含まれるOSSを利用する コンテナイメージに含まれるOSSを利用する
モデルDB S3 Azure DB

ストリーム型推論システム構築に向けたパブリッククラウドサービスの機能比較: 基盤層(ランタイム)

パブリッククラウドで本番環境を構築する場合は、ランタイムをコンテナで作成します。
デフォルトまたは自作のコンテナイメージを選択して、コンテナ作成時に必要なOSSを導入・更新します。
Azure MLではランタイム作成時にpipやcondaを利用して独自OSSを追加することもできます。

表2: パブリッククラウドサービスの機能比較: 基盤層(ランタイム)

項目 Amazon SageMaker Azure ML
ランタイムの種類 コンテナ コンテナ
デフォルトで提供されるコンテナイメージに含まれるOSSの種類 TensorFlow, PyTorch, Apache MXNet, Chainer, Keras, Gluon, Horovod, scikit-learn, および Deep Graph Library TensorFlow, PyTorch, Keras, scikit-learn, ONNX
上述のOSSの保守期限 言及なし(サポートページ参照) 言及なし(サポートページ参照)
ランタイム作成時のOSS追加方法(後編で実機検証) 事前にコンテナイメージを用意する 事前にコンテナイメージを用意する、またはランタイム作成時にnotebook経由でpipやcondaを利用して任意のOSSを追加する
ランタイムに含まれるOSSのアップデート可否 ×(新規ランタイム作成要) ×(新規ランタイム作成要)

ストリーム型推論システム構築に向けたパブリッククラウドサービスの機能比較: 基盤層(実行基盤)

Amazon SageMakerではAWSのマネージド環境にランタイムをデプロイしますが、Azure MLではランタイムの実行環境がコンテナ基盤(AKS)に限定されます。
Azure MLの基盤層はコンテナ基盤(AKS)の制約を受けるため、運用時はAKSの知識が必要といえます。
また、Amazon SageMakerとAzure MLの両方ともに可用性や運用・保守の機能についてはコンテナ基盤の機能に準拠していることがわかります。

表3: パブリッククラウドサービスの機能比較: 基盤層(実行基盤)

項目 Amazon SageMaker Azure ML
ランタイム実行環境(後編で実機検証) マネージド環境が提供される 別途コンテナ基盤(Azure Kubernetes Service)の用意が必要
コンテナの性能 コンテナホストに依存 コンテナホストに依存
コンテナホストの性能 2cpu/4GB ~ 96cpu/384GB 2cpu/4GB ~ 64cpu/256GB
コンテナ基盤(kubernetesクラスタ)のサイズ -(SageMakerの裏は意識しない) ノード3つ以上、クラスタ全体で12コア以上
複数ランタイムの並列処理 ×
1ランタイム内の並列処理 ×
GPUの利用
オートスケール △(AKSで可能)
障害発生時の自動復旧 〇(Amazon CloudWatchとの連携で可能) 〇(AKSによる自動復旧)
費用の上限値設定 AWS Budgetにてアラート Azure monitorにてアラート
ランタイムのモデル作成時と異なるリージョンへのデプロイ可否
ディザスタリカバリ × ×

おわりに

本投稿では、ストリーム型の機械学習システムをサービングする技術について、
Amazon SageMakerとAzure Machine Learningを比較した結果を紹介しました。
後編では、実際にストリーム型の機械学習システムを両クラウドでサービングして、本投稿で紹介した違いが現れること、
また、SEがどのような基準でAmazon SageMakerまたはAzure MLの利用を検討するべきかについて議論します。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

rails new〜デプロイまでの学習レポート

はじめに

本記事は、ぼくが2020年7月〜10月までに学習した、Ruby及びRailsの内容を振り返るものです。

今年の2月くらいの段階ではHTMLもまるで書けないくらい無知だったのですが、
等の学習サイトや各種記事を参考にさせていただいて、なんとかデプロイまで漕ぎ着けることができました。

なお現在の状況としては、

  • 就職活動中(HTMLコーダー職志望)
  • 学習を継続し、社内でのエンジニア登用を希望している
  • フロントエンドとインフラに関心が強い
  • Railsアプリの他に、Web制作に向けたWordPressの学習も行った
  • フロント・バックともにそれらサイトで学習の継続を予定

というところです。

制作したアプリケーション

動いている実際のアプリはこちら
使用言語はRuby、フレームワークはRuby on Railsです。

AWS EC2サーバーにて、

  • アプリケーションサーバ ▶︎ Puma
  • Webサーバ ▶︎ Nginx
  • データベース ▶︎ MySQL

という構成で動いています。

機能は現在

  • 映画作品のランダム表示機能
    スクリーンショット-2020-11-07-14.33.45-1024x587.png

  • 記事投稿機能
    スクリーンショット-2020-11-07-14.40.55-1024x587.png

以上の2つのみ。
正直ポートフォリオと呼んでいいのかさえ怪しいものではあります。

しかしながら、学習の目標を立てる際は「方向性だけわかるようにして機能は後回し、まずはデプロイまで一通りやる」ということを重点的に考えていたので、その目標自体は達成できました。本当に難しかったので、機能を書くより先にすませられたのは正直ホッとしています。

学習したこと

【1】Webサイトが動く大まかな仕組みについて

Rubyへの理解というよりは、Railsの仕組みに関して学ぶところが多かったように思えます。
フレームワーク側で効率化されている要素がかなり多く、大したRubyの知識がなくても何かしらの動作をするものは作れてしまうのが、すごいところでもあり、逆に油断してしまいそうだという印象です。

RailsはMVCというデザインパターンを採用しており、
「扱う情報を定義したクラス内で処理を記述する」
「SQL文によってデータベースから情報を取得する」
という概要に触れただけでも、Webサイトの動作に必要な処理の大まかなイメージを掴めたのはよかったと思っています。

【2】サーバーとデータベース

ユーザーからのリクエストを処理するWebサーバ(Nginx)と、Webサーバからのリクエストを受けてRailsを実行するアプリケーションサーバ(Puma)など、サーバーサイドの初歩的な技術を体験することができました。

AWS EC2で立てたインスタンスにNginxとPumaをインストールした構成にしており、データベースはRDSを使わずに、直接MySQLをインストールしてRailsと接続しています。

また、ターミナルからコマンドを使用したサーバーへのSSH接続も大変にいい経験でした。
FTPを使わなくてもサーバーに直接ログインし、viコマンドでファイルの書き換えなどを行うことができるようになりました。
作業の安全性を確保する上で、重要な知見になったと思います。

機能が少ない段階で一度デプロイしようと思ったのは、

  1. 初めは難しいだろうから、先に乗り越えてしまおうと思ったから
  2. 機能を実装しても、Web上で動作できなければ人に見せられないから
  3. 一度デプロイしてしまえば、その後はコードを書くことに集中できると判断したから
  4. さらにその過程で、Capistorano等の自動デプロイの方法も勉強できるから

というのが理由です。
実際unicornサーバが起動できずに何日も浪費してしまうなど、ローカル環境構築以上につまづくことの多い部分でした。公開が無事に完了しただけでもまずはよかったと思っています。

【3】フロントエンド

JavaScriptはProgateを一周しただけでしたが、少し苦手意識がありました。
処理が軽快になるというメリットもあって、動作させる部分はしばらくCSSアニメーションによって記述していたのですが、結果的にそれはjQueryを理解する大きな助けになりました。

いずれにせよクラスやidを使って直接的に動作を指定するという点では、両者に違いはないと気がつきました。
同時にCSSの場合、モバイル(タップ操作)での動作が厳密には定義されていない部分があり、レスポンシブ対応する上ではjQueryでなければ書けないこともあるのだ、というのも興味深い発見でした。

フロントエンドは現在特に関心を持っている分野であり、今後は基礎的なJavaScriptへの理解をもっと進めていきたいと考えています。
さらに、「jQueryによるDOM操作は規模が大きくなればなるほど面倒が増えていくだろう」という見当もついたため、今後はVue.jsによる柔軟な実装にも対応できるようになりたいと考えており、今後しばらくの学習目標として考えています。


今後の学習について

これらの経験を踏まえ、今後の個人学習は以下のような内容を考えています。

  1. JavaScriptを初め、RubyやPHPなどの動的型付け言語の基本文法

    1. 復習的な内容にはなりますが、一旦インフラは置いておき、まずはプログラミング言語自体の理解をどんどん深めていきたいです。
    2. JavaScriptの理解から派生して、フロントならVue.jsTypeScript、バックエンドはPHP等、学習内容を応用しながら順序立てて理解していきます。
  2. HTML・CSSに関する更なる理解

    1. HTML5及びCSS3の機能の多さに驚きました。SVG素材がとても使いやすく、音声や動画も簡単に挿入できたり、文字の折りたたみや計算などもHTMLだけで記述できます。CSSではアニメーションをつけたり、Sassによる効率化も可能です。
    2. これらを学習するだけでもレベルアップできますし、併せてJavaScriptの学習も続ければ、すぐに複雑なサイトを作れるようになると思います。
  3. デプロイの自動化
     1.Circle CICapistranoを使った自動デプロイももちろんですが、今一番やりたいのはWordPressテーマのソースコードを変更した際、GitHubのmasterブランチにPushした時点で自動的に反映できるようにすることです。
    Railsアプリで同様の自動化を行う入門もかねて、WordPressによるコーディング・フロントエンド学習を効率化したいと考えています。

最後に

7月に前職を退職してから現在に至るまで、学習においては多数の方のお世話になっております。
Qiitaやブログ、オウンドメディア等にあるたくさんの記事を参考にさせていただきましたし、teratailで拙い質問に答えていただいたこともありました。

皆様にはこの場を借りてお礼を申し上げます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[OCI]goofysを利用してAWS S3からオブジェクト・ストレージに直接ファイルをコピーしてデータ移行してみた

はじめに

オブジェクト・ストレージをファイルシステムとしてマウントできるgoofysを利用して、AWSのS3バケットからOCIオブジェクト・ストレージに直接データを移行してみました。

この方法であれば、S3バケット内のファイルを一度どこかにダウンロードしてからアップロードする必要がないため、中間ストレージが不要になります。

安定した環境が必要な場合は、S3側にAWS Storage Gateway、OCIオブジェクト・ストレージ側にOracle Cloud Infrastructure Storage Gatewayを使用することをお勧めします。

事前準備

AWS側(データ移行元)
・S3バケット(ここではfrom-aws-bucketという名前です)
・S3バケットにアクセスできるユーザのアクセスキーとシークレットキー
OCI(データ移行先)
・オブジェクト・ストレージ・バケット(ここではociという名前です)
・オブジェクト・ストレージ・バケットにアクセスできるユーザの顧客秘密キー(アクセス・キーと秘密キー)
・インターネットアクセスが可能なコンピュート・インスタンス(今回はOSとしてOracle Linux 7.9を使用)

1. goofysのインストールと設定

SSHクライアントからコンピュート・インスタンスにopcユーザでログインします。

goofysはGo、fuseを使用するので、事前にインストールします。

sudo yum install -y golang fuse

goofysのバイナリをダウンロードします。

wget https://github.com/kahing/goofys/releases/latest/download/goofys

goofysをパスが通っているディレクトリに移動します。

sudo cp goofys /usr/local/bin

オーナーをrootに変更し、パーミッションを変更します。

sudo chown root:root /usr/local/bin/goofys
sudo chmod 775 /usr/local/bin/goofys

goofysが使用する認証情報の設定ファイルを作成します。

sudo mkdir -p /root/.aws
sudo vi /root/.aws/credentials
/root/.aws/credentials
[oci]
aws_access_key_id = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA <- OCIユーザのアクセスキー
aws_secret_access_key = BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB <- OCIユーザの秘密キー

[aws]
aws_access_key_id = CCCCCCCCCCCCCCCCCCCC <- AWSユーザのアクセスキー
aws_secret_access_key = DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD <= AWSユーザのシークレットキー

以上でgoofysを使用する準備が整いました。

2. goofysによるS3バケットとオブジェクト・ストレージ・バケットのマウント

マウントポイントを作成します。
/mnt_o:OCI オブジェクト・ストレージ・バケット(oci)用のマウントポイント
/mnt_a:AWS S3バケット(from_aws_bucket)用のマウントポイント

sudo mkdir -p /mnt_o
sudo chmod -R 777 /mnt_o
sudo mkdir -p /mnt_a
sudo chmod -R 777 /mnt_a

/etc/fstabを編集します。

sudo vi /etc/fstab

以下の2行をファイルの末尾に追加します。

goofys#oci /mnt_o fuse _netdev,allow_other,--dir-mode=0755,--file-mode=0666,--uid=1000,--gid=1000,--endpoint=https://<ネームスペース>.compat.objectstorage.<OCIリージョン識別子>.oraclecloud.com,--region=<OCIリージョン識別子>,--profile=oci 0 0
goofys#from-aws-bucket /mnt_a fuse _netdev,allow_other,--region=<AWSリージョン識別子>,--profile=aws 0 0

mount -aでfstabの内容を反映し、OCIのオブジェクト・ストレージ・バケット oci/mnt_oに、AWSのS3バケット from_aws_bucket/mnt_aにマウントします。

sudo mount -a

3. S3バケットからオブジェクト・ストレージ・バケットへのデータコピー

AWSのコンソールから、S3バケット from_aws_bucket の内容を確認します。
スクリーンショット 2020-11-17 23.47.04.png
テスト用のPDFファイルが30個入っています。

S3バケット from_aws_bucket をマウントした /mnt_a の内容を確認します。

[opc@iptest /]$ cd /mnt_a
[opc@iptest mnt_a]$ ls -l
total 63990
-rw-r--r--. 1 root root 2183884 Nov 17 14:36 50544 10.pdf
-rw-r--r--. 1 root root 2183884 Nov 17 14:36 50544 11.pdf
-rw-r--r--. 1 root root 2183884 Nov 17 14:36 50544 12.pdf
-rw-r--r--. 1 root root 2183884 Nov 17 14:36 50544 13.pdf
-rw-r--r--. 1 root root 2183884 Nov 17 14:36 50544 14.pdf
-rw-r--r--. 1 root root 2183884 Nov 17 14:36 50544 15.pdf
-rw-r--r--. 1 root root 2183884 Nov 17 14:36 50544 16.pdf
-rw-r--r--. 1 root root 2183884 Nov 17 14:36 50544 17.pdf
-rw-r--r--. 1 root root 2183884 Nov 17 14:36 50544 18.pdf
-rw-r--r--. 1 root root 2183884 Nov 17 14:36 50544 19.pdf
-rw-r--r--. 1 root root 2183884 Nov 17 14:36 50544 1.pdf
-rw-r--r--. 1 root root 2183884 Nov 17 14:36 50544 20.pdf
-rw-r--r--. 1 root root 2183884 Nov 17 14:36 50544 21.pdf
-rw-r--r--. 1 root root 2183884 Nov 17 14:36 50544 22.pdf
-rw-r--r--. 1 root root 2183884 Nov 17 14:36 50544 23.pdf
-rw-r--r--. 1 root root 2183884 Nov 17 14:36 50544 24.pdf
-rw-r--r--. 1 root root 2183884 Nov 17 14:36 50544 25.pdf
-rw-r--r--. 1 root root 2183884 Nov 17 14:36 50544 26.pdf
-rw-r--r--. 1 root root 2183884 Nov 17 14:36 50544 27.pdf
-rw-r--r--. 1 root root 2183884 Nov 17 14:36 50544 28.pdf
-rw-r--r--. 1 root root 2183884 Nov 17 14:36 50544 29.pdf
-rw-r--r--. 1 root root 2183884 Nov 17 14:36 50544 2.pdf
-rw-r--r--. 1 root root 2183884 Nov 17 14:36 50544 30.pdf
-rw-r--r--. 1 root root 2183884 Nov 17 14:36 50544 3.pdf
-rw-r--r--. 1 root root 2183884 Nov 17 14:36 50544 4.pdf
-rw-r--r--. 1 root root 2183884 Nov 17 14:36 50544 5.pdf
-rw-r--r--. 1 root root 2183884 Nov 17 14:36 50544 6.pdf
-rw-r--r--. 1 root root 2183884 Nov 17 14:36 50544 7.pdf
-rw-r--r--. 1 root root 2183884 Nov 17 14:36 50544 8.pdf
-rw-r--r--. 1 root root 2183884 Nov 17 14:36 50544 9.pdf

S3バケット from_aws_bucket の内容が表示されることを確認できました。

では、cpコマンドで /mnt_a 内のファイルを /mnt_o にコピーしてみます。

cp /mnt_a/* /mnt_o

コピーが完了したら、オブジェクト・ストレージ・バケット oci をマウントした /mnt_o の内容を確認します。

[opc@iptest mnt_a]$ cd /mnt_o
[opc@iptest mnt_o]$ ls -l
total 63990
-rw-r--r--. 1 root root 2183884 Nov 17 14:39 50544 10.pdf
-rw-r--r--. 1 root root 2183884 Nov 17 14:39 50544 11.pdf
-rw-r--r--. 1 root root 2183884 Nov 17 14:39 50544 12.pdf
-rw-r--r--. 1 root root 2183884 Nov 17 14:39 50544 13.pdf
-rw-r--r--. 1 root root 2183884 Nov 17 14:39 50544 14.pdf
-rw-r--r--. 1 root root 2183884 Nov 17 14:39 50544 15.pdf
-rw-r--r--. 1 root root 2183884 Nov 17 14:39 50544 16.pdf
-rw-r--r--. 1 root root 2183884 Nov 17 14:39 50544 17.pdf
-rw-r--r--. 1 root root 2183884 Nov 17 14:39 50544 18.pdf
-rw-r--r--. 1 root root 2183884 Nov 17 14:39 50544 19.pdf
-rw-r--r--. 1 root root 2183884 Nov 17 14:39 50544 1.pdf
-rw-r--r--. 1 root root 2183884 Nov 17 14:39 50544 20.pdf
-rw-r--r--. 1 root root 2183884 Nov 17 14:39 50544 21.pdf
-rw-r--r--. 1 root root 2183884 Nov 17 14:39 50544 22.pdf
-rw-r--r--. 1 root root 2183884 Nov 17 14:40 50544 23.pdf
-rw-r--r--. 1 root root 2183884 Nov 17 14:40 50544 24.pdf
-rw-r--r--. 1 root root 2183884 Nov 17 14:40 50544 25.pdf
-rw-r--r--. 1 root root 2183884 Nov 17 14:40 50544 26.pdf
-rw-r--r--. 1 root root 2183884 Nov 17 14:40 50544 27.pdf
-rw-r--r--. 1 root root 2183884 Nov 17 14:40 50544 28.pdf
-rw-r--r--. 1 root root 2183884 Nov 17 14:40 50544 29.pdf
-rw-r--r--. 1 root root 2183884 Nov 17 14:40 50544 2.pdf
-rw-r--r--. 1 root root 2183884 Nov 17 14:40 50544 30.pdf
-rw-r--r--. 1 root root 2183884 Nov 17 14:40 50544 3.pdf
-rw-r--r--. 1 root root 2183884 Nov 17 14:40 50544 4.pdf
-rw-r--r--. 1 root root 2183884 Nov 17 14:40 50544 5.pdf
-rw-r--r--. 1 root root 2183884 Nov 17 14:40 50544 6.pdf
-rw-r--r--. 1 root root 2183884 Nov 17 14:40 50544 7.pdf
-rw-r--r--. 1 root root 2183884 Nov 17 14:40 50544 8.pdf
-rw-r--r--. 1 root root 2183884 Nov 17 14:40 50544 9.pdf

/mnt_a(=S3バケット from_aws_bucket )の内容が /mnt_o にコピーされました。

OCIのコンソールから、オブジェクト・ストレージ・バケット oci の内容を確認します。
スクリーンショット 2020-11-17 23.45.48.png
S3バケット from_aws_bucket の内容がオブジェクト・ストレージ・バケット ociにコピーされていることが確認できました。

まとめ

goofysを使用することで、AWSのS3バケットの内容を一度ダウンロードすることなく、OCIのオブジェクト・ストレージ・バケットに直接コピーしてデータを移行することができました。めでたし、めでたし。

ファイル数やファイルサイズによるパフォーマンスの変化についても、後日確認してみたいと思います。

参考文献

goofys
Amazon S3互換API(OCIオブジェクト・ストレージ)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む