プログラマーのメモ書き

伊勢在住のプログラマーが気になることを気ままにメモったブログです

AWS CLI 複数の認証情報の設定について

以前、EC2のインスタンスを t1.micro から t2.nano へ移行する作業について書きました。

blog.mori-soft.com

この中で、S3 へバックアップを行うという設定を行っていたのですが、月一動作だったので、改めて動作確認をしてみると、どうもうまく動いていません。 こんな感じのエラーです。

2017-06-01 03:05:01 JST: aws s3 cp バックアップファイル名 s3://バケット名/
upload failed: バックアップファイル名 to s3://バケット名/ファイル名 An error occurred (AccessDenied) when calling the CreateMultipartUpload operation: Access Denied

アクセス拒否のようです。

aws cli はいろんな方法で認証情報を指定できるので、これが間違っているのだろうと予想されます。 いろいろと調べてみると、 aws configure list で現在の認証情報の設定状況を確認できることがわかりました。

dev.classmethod.jp

早速、試してみると

bitnami@ip-172-30-0-73:~/.aws$ aws configure list
      Name                    Value             Type    Location
      ----                    -----             ----    --------
   profile                <not set>             None    None
access_key     ****************AAAA shared-credentials-file    
secret_key     ****************aaaa shared-credentials-file    
    region                <not set>             None    None
bitnami@ip-172-30-0-73:~/.aws$ 

あれ?なんかおかしいぞ。

実は、aws cli を使う際、EC2 用のユーザーと S3 用のユーザーを分けていました。 で、上記のアクセスキーなどは、EC2ユーザーのものです。 おまけに、 region も設定されていない。

ということで、認証情報の設定方法を見直しました。

間違っていた点

恥ずかしならが、調べた結果、いろいろと間違っていました。まず現在設定していた方法は、

  • EC2 用のユーザーは aws configure コマンドで設定する
    • .aws/config と .aws/credentials に内容が書かれている
  • S3 用のユーザーは .aws/config.s3 ファイルにすべての内容を記述する
  • S3 用のユーザーを使う場合は、 環境変数で AWS_DEFAULT_CONFIG=.aws/config.s3 を指定する

というものでした。

で、まず大きく間違っていたのが、下記の説明にある、

docs.aws.amazon.com

この部分、

Storing Credentials in Config
The AWS CLI will also read credentials from the config file. If you want to keep all of your profile settings in a single file, you can. If there are ever credentials in both locations for a profile (say you used aws configure to update the profile's keys), the keys in the credentials file will take precedence.

(頑張って意訳)

Config ファイルへの認証情報の保存について
AWS CLI は config ファイルから認証情報を読むこともできます。プロファイル設定のすべてを1ファイルに保存することも可能です。もし、プロファイルの情報が複数個所にある場合は、credentialsファイルのキーが優先されます。

です。

この前半部分を読んで、configファイルに認証情報全部を書けると思い込んでしまいました。でも、上記の意訳からもわかるように、書けることはかけるんですが、(同じプロファイル)に対する認証情報が複数ある場合、credentialsファイルのものが優先的に使われるそうです。 やられました。

更なる間違い

上記の問題が大きいので、些細なことですが、さらに、ついでに間違っていたのが、config.s3 ファイルの書き方です。 config.s3 ファイルを、

bitnami@ip-172-30-0-73:~/.aws$ cat config.s3
[s3]
aws_access_key_id = アクセスキー
aws_secret_access_key = シークレットアクセスキー
region = ap-northeast-1
bitnami@ip-172-30-0-73:~/.aws$ 

と書いており、いつつけたか覚えのない s3 というプロファイル名がついていました。 これは、プロファイル名になるはずなので、切り替えたければ、 --profile s3 のようにコマンド実行時にプロファイル名も指定する必要がありました。

仮にここを [default] に修正すると、

bitnami@ip-172-30-0-73:~/.aws$ aws configure list
      Name                    Value             Type    Location
      ----                    -----             ----    --------
   profile                <not set>             None    None
access_key     ****************AAAA shared-credentials-file    
secret_key     ****************aaaa shared-credentials-file    
    region           ap-northeast-1      config-file    /home/bitnami/.aws/config.s3
bitnami@ip-172-30-0-73:~/.aws$ 

のように、regionは正しく認識されました(もっとも、上記の二重に認証情報がある場合の問題が残っているので、これでも使えませんがね)。

最終的な設定内容

で、結局、認証情報を使い分けるために、プロファイルを追加することとしました。ここで、プロファイル名は s3user としています。 設定後のファイルはこんな感じになります。

~/.aws/credentials

[default]
aws_access_key_id = EC2アクセス用のアクセスキー
aws_secret_access_key = EC2アクセス用のシークレットアクセスキー

[s3user]
aws_access_key_id = S3アクセス用のアクセスキー
aws_secret_access_key = S3アクセス用のシークレットアクセスキー

~/.aws/config

[default]
region = ap-northeast-1

[profile s3user]
region = ap-northeast-1

--profile s3user をつけてコマンドを実行すると、S3 用ユーザーの認証情報が使われています。

bitnami@ip-172-30-0-73:~/.aws$ aws configure list --profile s3user
      Name                    Value             Type    Location
      ----                    -----             ----    --------
   profile                   s3user           manual    --profile
access_key     ****************BBBB shared-credentials-file    
secret_key     ****************bbbb shared-credentials-file    
    region           ap-northeast-1      config-file    ~/.aws/config
bitnami@ip-172-30-0-73:~/.aws$ 

念のために、 --profile なしを試すと、ちゃんと EC2 用ユーザーの認証情報が使われます。

bitnami@ip-172-30-0-73:~/.aws$ aws configure list                                                                                                                     
      Name                    Value             Type    Location
      ----                    -----             ----    --------
   profile                <not set>             None    None
access_key     ****************AAAA shared-credentials-file    
secret_key     ****************aaaa shared-credentials-file    
    region           ap-northeast-1      config-file    ~/.aws/config
bitnami@ip-172-30-0-73:~/.aws$ 

この認証情報を使うようにバックアップスクリプトを修正したら、問題なく動作しました。 めでたし、めでたし。

mapsforge でポップアップするマーカーを試す

下記の記事で、 Android でのオフライン地図表示を試しましたが、

blog.mori-soft.com

本格的に進めるために、まずは地図表示ライブラリの mapsforge をもう少し詳しく使ってみたいと思います。

簡単なサンプル

まず最初に、簡単なサンプルとして、下記のリンク先の説明に従って、地図を表示してみます。

https://github.com/mapsforge/mapsforge/blob/master/docs/Getting-Started-Android-App.md

英語ですが、書いてある通りにすればよいので、そんなに難しくはありません。

地図データのダウンロード

地図データについては、ベルリンの地図を表示してもピンとこないので、日本の地図をダウンロードしておきます。 ちなみに、今回は multilingual 以下にある地図を使いました。

今回のアプリは実機(Nexus5, Android 6.0)で試そうと思います。なので、実機にファイルを転送しておきます。

C:\> adb push japan-multi.map /sdcard/Download/

ファイルの保存先(今回は /sdcard/Download/ )は、実装する予定のコードに合わせます。

依存関係の記述

開発環境として Android Studio を使っているので、ライブラリファイルの依存関係を下記 Integration Guide を参考に

https://github.com/mapsforge/mapsforge/blob/master/docs/Integration.md

build.gradle に書いておきます。 基本的に、 Core と Android のセクションのものを書いておけばいいはずです。下記のようになりました。

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:25.2.0'

    compile 'org.mapsforge:mapsforge-core:0.8.0'
    compile 'org.mapsforge:mapsforge-map:0.8.0'
    compile 'org.mapsforge:mapsforge-map-reader:0.8.0'
    compile 'org.mapsforge:mapsforge-themes:0.8.0'
    compile 'net.sf.kxml:kxml2:2.3.0'

    compile 'org.mapsforge:mapsforge-map-android:0.8.0'
    compile 'com.caverock:androidsvg:1.2.2-beta-1'
}

コードの記述

で、リンク先の指示に従ってサンプルコードを書くのですが、今回試した実機(Nexus5)がたまたま Android 6.0 だったため、外部ストレージ領域へのアクセスに手間取りました。 というのも、API レベル 23 以上がターゲットの場合、パーミッションをインストール時に許可するのではなく、実行時に許可するようになってました。

developer.android.com

ここだけ気をつければ、あとは大きな問題なく動作するはずです。 動作時のイメージはこんな感じです。

f:id:junichim:20170530172659p:plain

なお、参考までにサンプルコードGitHubにあげておきます。

https://github.com/samples-of-junichim/MapsforgeSample.git

このリポジトリのタグ simple になります。

サンプルプログラムの動作

さて、次はマーカーを表示してみたいと思います。

ただ、参考になるものが、GitHub の mapsforge の説明にはみあたらないので、一度 mapsforge のリポジトリを clone して、この中に含まれている mapsforge-samples-android を動かしてみます。

androidのサンプルプログラムは、リポジトリを clone 後、 Android Studio にインポートすればOKです。 なお、インポートの際は、 mapsforge-samples-android ではなく、リポジトリのトップディレクトリを指すようにしてください(mapsforge-samples-android を指してもうまく認識されません)。

問題がなくプロジェクトが立ち上がったら、実機を接続して実行してみてください。なお、このサンプルの実行には、germany.map の地図ファイルが必要になりますので、別途ダウンロードして、実機の /sdcard にコピーしておきます。

このサンプルプログラムを動作させるといろいろなデモを見ることができます。

マーカーの表示には、このうち、OverlayMapViewer.java を参考にしました。

f:id:junichim:20170530173634p:plain

実行するとこんな感じの表示になります。

f:id:junichim:20170530173800p:plain

なお、このサンプルでは、いろんなものを地図上に表示する方法を示しています。円や折れ線、画像(いわゆるマーカー)、画像で領域を埋める、などです。参考になりますね。

マーカーの表示

さて、上記のソースコードをざっと読んで、実際に試してみましょう。上記のソースコードから、マーカーを表示するためには、 Marker というクラスをインスタンス化して、MapView に追加すれば良さそうです。

mapView.getLayerManager().getLayers().add(marker);

Marker をインスタンス化する際には、表示したい画像を Drawable から、 Bitmap(org.mapsforge.core.graphics.Bitmap)に変換してやる必要があります。 このあたりの処理も、先ほどのサンプルコードに載っていて、

        Drawable drawable = getResources().getDrawable(resource);
        Bitmap bitmap = AndroidGraphicFactory.convertToBitmap(drawable);

とすればOKです。 なお、AndroidGraphicFactory.convertToBitmap メソッドは、android.graphics.Bitmap を内部に保持する AndroidBitmap クラス(org.mapsforge.core.graphics.Bitmap インターフェースを実装)を返しています。

今回作成したサンプルの動作としては、画面上をロングタップすると、その位置にマーカーを設置する、というものです。 最初のサンプルでも出てきた、TileRendererlayer の onLongPress をオーバーライドして、そこからマーカーを設置するメソッドを呼び出しています。

        TileRendererLayer trl = new TileRendererLayer(tileCache, mds, mapView.getModel().mapViewPosition, AndroidGraphicFactory.INSTANCE) {
            @Override
            public boolean onLongPress(LatLong tapLatLong, Point layerXY, Point tapXY) {
                return setMarker(tapLatLong);
            }
        };

動作イメージは下記のようになります。

f:id:junichim:20170530175048p:plain

この部分のソースコードは、リポジトリ

https://github.com/samples-of-junichim/MapsforgeSample.git

の marker タグを見てください。

ポップアップを追加

次は、マーカーをタップすると、ポップアップを表示するという処理を試したいと思います。

これって、地図系のアプリでは当たり前のように使われてるとおもうんだけど、ライブラリとかだとたまにサポートされていなかったりします。 で、mapsforge はどうかというと、ああ、残念、組み込み済みの便利な実装はありませんでした。 というわけで、自分で実装します。

ポップアップの挙動もいろんな方法があると思うのですが、今回は、次のように動作するポップアップを作成します。

  • マーカーをタップするとポップアップが表示される
  • ポップアップが表示中の場合は、再度同じマーカーがタップされるとポップアップを閉じる
  • マーカーのない場所をタップするとポップアップが閉じる
  • ポップアップ表示中に、別のマーカーをタップすると後者のマーカーのポップアップのみを表示する(つまり、ポップアップの表示をマップ全体で最大一つのマーカーだけに限定する)

さて、実装に先立ち、ヒントになるものを、 mapsforge-samples-android から探すと、BubbleOverlay.java と ViewOverlayViewer.java が参考になりそうです。

BubbleOverlay.java を見ると、マップ上に吹き出しが表示されています。この吹き出しは、いったんTextViewを作成して、それをビットアップに変換して、Makerにして表示するというものです。

ViewOverlayViewer.java でやってるのは、View(Button)を作って、これをMapView(ViewGroupの派生)に追加して表示させるというものです。

今回は、BubbleOverlay の方法を参考にしました。 やり方はいたってシンプルで、最初に地図上をロングタップして Marker を表示するところは同じです。で、その Marker のタップイベントのリスナーにおいて、ポップアップを表示する処理を行います。

具体的には、 Marker を extends したクラスを作り onTap にて

    @Override
    public boolean onTap(LatLong geoPoint, Point viewPosition, Point tapPoint) {

        Log.d(TAG, "LoaLong: " + geoPoint.getLatitude() + ", " + geoPoint.getLongitude());
        Log.d(TAG, "viewPos: " + viewPosition.x + ", " + viewPosition.y);
        Log.d(TAG, "tapPos : " + tapPoint.x + ", " + tapPoint.y);

        if (contains(viewPosition, tapPoint)) {
            Log.d(TAG, "contains: " + true);

            if (! TextUtils.isEmpty(mText)) {
                Log.d(TAG, "text is exist");

                if (null == mBalloonMarker) {
                    Log.d(TAG, "balloon is null");

                    Bitmap bmp = createBalloon(mMapView.getContext());
                    mBalloonMarker = new Marker(MarkerWithBubble.this.getLatLong(), bmp, 0, - bmp.getHeight() / 2 - BALLOON_VERTICAL_OFFSET );
                    mMapView.getLayerManager().getLayers().add(mBalloonMarker);
                } else {

                    if (null != mBalloonMarker) {
                        mBalloonMarker.setVisible(!mBalloonMarker.isVisible());
                    }
                }
            }
            Log.d(TAG, "text is null");
        } else {
            Log.d(TAG, "contains: " + false);

            if (null != mBalloonMarker) {
                mBalloonMarker.setVisible(false);
            }
        }
        return super.onTap(geoPoint, viewPosition, tapPoint);
    }

のようにします。

今回は、ポップアップの表示をマップ全体で最大一つのマーカーだけに限定したいというのがあったため、タップイベントが一つのMakerで終わらないように、ポップアップの処理を行っても、戻り値として true を返していません。 ここが個人的には、ちょっと気になるところです。もっといい実装方法はないかなー。

さて、上記関数で呼び出している createBalloon は

    private Bitmap createBalloon(Context c) {
        TextView tv = (TextView) LayoutInflater.from(mMapView.getContext()).inflate(R.layout.popup_marker, null);
        tv.setTextColor(mColor);
        tv.setTextSize(mTextSize);
        tv.setText(mText);

        return Utils.viewToBitmap(c, tv);
    }
public class Utils {

    public static Bitmap viewToBitmap(Context c, View view) {
        view.measure(View.MeasureSpec.getSize(view.getMeasuredWidth()),
                View.MeasureSpec.getSize(view.getMeasuredHeight()));
        view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight());
        view.setDrawingCacheEnabled(true);
        Drawable drawable = new BitmapDrawable(c.getResources(),
                android.graphics.Bitmap.createBitmap(view.getDrawingCache()));
        view.setDrawingCacheEnabled(false);
        return AndroidGraphicFactory.convertToBitmap(drawable);
    }

}

のように、さきほど挙げた BalloonOverlay.java で使っていた処理を参考にTextViewをBitmapに変換しています。

実際に動かしてみると、

f:id:junichim:20170530215347p:plain

のようになります。

参考

このポップアップするマーカーですが、最初は、 ViewOverlayViewer.java を参考に、バルーンを背景に持つTextViewをMapViewに追加して実装しようと考えました。 ポップアップの表示や複数マーカーに対する切り替えはうまくいったのですが、表示位置をずらすことがうまくできなくてこちらの方法ではあきらめました。

MapView に View を追加する際に、 MapView.LayoutParams でレイアウトパラメータを指定しています。このクラスは ViewGroup.LayoutParams の派生クラスで、オフセットを与えることができないようです。 さらに、 MapView#onLayout では子 View が MapView.LayoutParams を持っているのを前提に処理を行っているようなので、あきらめました。

リポジトリの 62f3131 あたりにこの方法で試した痕跡があるので、気になる方はぜひチャレンジしてみてください。

課題

一応、これで mapsforge でマーカーの表示とタップによるポップアップ表示ができるようになりました。 今回のサンプルでは、ロングタップでマーカーを設定する、という組み方をしたため、マーカーを設定した場所の情報がなく、そのため、画面回転で再描画が行われるとマーカーが消えてしまうという問題があります。 実際の利用時は、この辺りはリスト等で管理してあげないといけなさそうです。

OSC名古屋 2017 に参加してきました

先日(2017年5月27日(土))に行われた、オープンソースカンファレンス名古屋に伊勢志摩コミュニティ共同ブースとして参加してきました。

元々、出展側で参加する気はなかったのですが、伊勢IT交流会によく来られている Kapper さんからお話を振られて、コミュニティでも参加できるよ、ってことを教えていただいたので、ほとんどノリだけで参加してきました。 とはいっても、伊勢IT交流会 単独で参加してもそんなにアピールするものもないので、この地域で活動している他のコミュニティさんも巻き込んで共同ブースという形で出てきました。 ちょうど、『名古屋勉強会・コミュニティ協同ブース 』さんが出展していたので、それを真似させていただきました。

当日の様子

共同ブースは、

の3コミュニティで参加してきました。 チーム伊勢さんと伊勢ギークフェアさんはチラシのみで参加。

こんな感じです。

f:id:junichim:20170529122037j:plain

正直、一地方のコミュニティなんで、そんなに人も来ないだろうと思っていました。 ですが、名古屋という場所柄か、三重県に縁のある方が結構来られて、なんと、準備していたチラシがすべてなくなってしまいまい、急ぎで1階の事務室を借りてコピーを増刷することになりました。うれしい悲鳴ですね。

懇親会でもいろんな方とお話ができてよかったです。

次回の開催予定は秋ごろを考えているのですが、これをきっかけに県内から(もちろん県外からも)来る人が増えるといいなと思います。

参考

いろんな方が参加レポートや出展レポートを書かれているので、興味ある方はぜひ読んでみてください。 ここでは、今回の参加のきっかけとなった Kapper1224 さんのブログを貼っておきますね。

kapper1224.sblo.jp