facebookのアクセストークンの長さについて
元記事が変更されてました。(2013/5/3)
現在は、MySQLのTEXT型にしておいたほうが安全なようです。
facebookのOAuth認証を行うアプリを作る場合、access tokenを保存しなければならないかと思います。
テストアカウントで認証を行なっても、それぞれバラバラの長さで取得されるため、必要十分な長さがわかりませんでした。
本番で変わったユーザのみ認証が成功しないなど、気づきにくいバグの発生を誘発する恐れもあります。
(実際、varchar(200)で定義してて、1つのテストアカウントでは投稿できないということがありました。)
そこで、いろいろ調べてみたのですが、stackoverflowに書いてありました。
http://stackoverflow.com/questions/4408945/what-is-the-length-of-the-access-token-in-facebook-oauth2
元記事はこちら。
https://developers.facebook.com/blog/post/572/
なので、MySQLなどで保存する場合は
varchar(255)
としておけば問題ないそうです。
案外、検索しても出て来なかったので、メモ。
Read It Nowをリリースしました。
Read It Now
https://play.google.com/store/apps/details?id=hm.orz.chaos114.android.readitnow
Pocket (Formerly Read It Later) の非公式Androidウィジェットです。
・タグ・検索文字列などの条件で絞り込んだ件数を、ウィジェットに表示
・ウィジェット毎に条件を保存可能
・ウィジェットをタップすると、絞り込んだ一覧を表示
※指定できる条件
state(all or unread or Archive)
favorite(favorited or un-favorited)
tag(all or 入力したタグ名)
contentType(article or video or image)
sort(newest or oldest or title or site)
また、このアプリケーションも全てのソースを公開しています。
https://github.com/noboru-i/ReadItNow
技術的なトピックとしては
・Pocket API の利用
・ActionBarSherlock の利用
本アプリケーションは、Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) の下で提供されるソースコードを使用しています。
Pocket公式クライアントを利用していたのですが、あとで読もうと思って忘れてしまうことが多々ありました。
しかし、RSSはGoogle Readerのwidgetを利用していたので、Home画面に未読件数が表示されているので、ちょっとした空き時間に思い出して消化していくことが出来ました。
そこで、Pocketの未読件数もHome画面に表示しようと思ったのですが、Pocket公式クライアントにはwidgetがありません。
無いものは作ろうと思い、作ったのがこれです。
stateがunreadのものだけ表示しておけば、未読件数がHome画面に表示され続けるので、Google Readerのwidgetと同じような使い方が出来るかと思います。
とは言いつつ、未読を既読にする機能が未実装なので、Pocket公式クライアントを利用しないと管理は面倒です。
こちらについては、アプリ内で更新も出来るように改善予定です。
ちなみにこのアプリ、Night_Hack_Cero_v01参加中に公開しました。
Ver2.0もあるようなので、参加を検討中です。
タップしたらActivityが起動するwidgetを作成しました
全てのソースコードは下記より
https://github.com/noboru-i/ReadItNow/tree/tap-action-widget
最終的に、下記のように比較的すっきりしました。
プログラミング途中では、思ったように動かず、それを解決するためにもっとコード量が多かったのですが、
アンインストール→インストール→widgetの配置
を毎回するようにすると、うまいこと動きました。
変更し、上書きインストールなどを行うと、AppWidgetProvider#onUpdateが呼び出されたりして、
想定とは異なった動きになっていたようです。
実装
まず、AndroidManifest.xml にwidgetを定義します。
<receiver android:name=".appwidget.CountWidget" android:exported="false" > <intent-filter> <action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> </intent-filter> <!-- This specifies the widget provider info --> <meta-data android:name="android.appwidget.provider" android:resource="@xml/widgetinfo" /> </receiver>
- android.appwidget.action.APPWIDGET_UPDATE
はウィジェットの更新を取得するために必ず必要となります。
- widgetinfo
はxmlフォルダに定義します。
widgetinfo.xml
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android" android:initialLayout="@layout/widget_layout" android:minHeight="40dp" android:minWidth="40dp" android:resizeMode="vertical|horizontal" android:updatePeriodMillis="1800000" android:configure="hm.orz.chaos114.android.readitnow.ui.SettingActivity" />
- android:initialLayout
widgetのレイアウトを指定します。
https://sites.google.com/a/techdoctranslator.com/jp/android/practices/ui_guidelines/widget_design
より、1セルは40dpのようなので、40dpを指定しています。
- android:configure
には、widget配置時に起動するActivityを指定しています。パッケージ名を含め、全てを記載します。
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_margin="@dimen/widget_margin" android:background="@drawable/widget_background" android:orientation="vertical" > 略 </LinearLayout>
- android:layout_margin
https://sites.google.com/a/techdoctranslator.com/jp/android/practices/ui_guidelines/widget_design
より、8dpぐらいマージンを取ったほうがよさそうなので、"@dimen/widget_margin"を指定しています。
values/dimens.xml に定義しました。
CountWidget.java
package hm.orz.chaos114.android.readitnow.appwidget; import hm.orz.chaos114.android.readitnow.R; import hm.orz.chaos114.android.readitnow.ui.MainActivity; import android.app.PendingIntent; import android.appwidget.AppWidgetManager; import android.appwidget.AppWidgetProvider; import android.content.Context; import android.content.Intent; import android.util.Log; import android.widget.RemoteViews; public class CountWidget extends AppWidgetProvider { private static final String TAG = CountWidget.class.getSimpleName(); public static final String EXTRA_APP_WIDGET_ID = "appWidgetId"; @Override public void onEnabled(Context context) { Log.d(TAG, "#onEnabled"); } @Override public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { Log.d(TAG, "#onUpdate"); Log.d(TAG, "appWidgetIds.length = " + appWidgetIds.length); Log.d(TAG, "appWidgetIds[0] = " + appWidgetIds[0]); for (int appWidgetId : appWidgetIds) { RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.widget_layout); Intent intent = new Intent(context, MainActivity.class); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); PendingIntent pendingIntent = PendingIntent.getActivity(context, appWidgetId, intent, 0); remoteViews.setOnClickPendingIntent(R.id.text_view, pendingIntent); appWidgetManager.updateAppWidget(appWidgetId, remoteViews); } } @Override public void onDeleted(Context context, int[] appWidgetIds) { Log.d(TAG, "#onDeleted"); } @Override public void onDisabled(Context context) { Log.d(TAG, "#onDisabled"); } }
- #onUpdate
複数個のwidgetを配置し、再インストールした際など、
引数のappWidgetIdsには複数件入ってくるようなので、ループで処理しています。
PendingIntent を作成し、RemoteViews#setOnClickPendingIntent に渡します。
第2引数については、
http://y-anz-m.blogspot.jp/2011/07/androidappwidget-pendingintent-putextra.html
を参考にidを指定しました。
SettingActivity.java
package hm.orz.chaos114.android.readitnow.ui; import hm.orz.chaos114.android.readitnow.R; import android.appwidget.AppWidgetManager; import android.content.Intent; import android.os.Bundle; import android.preference.PreferenceActivity; import android.util.Log; import android.view.View; public class SettingActivity extends PreferenceActivity { private static final String TAG = SettingActivity.class.getSimpleName(); private int mAppWidgetId; @SuppressWarnings("deprecation") @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); addPreferencesFromResource(R.layout.query_preference); // Find the widget id from the intent. Intent intent = getIntent(); Bundle extras = intent.getExtras(); if (extras != null) { mAppWidgetId = extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID); } // If they gave us an intent without the widget id, just bail. if (mAppWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) { finish(); } ButtonPreference preference = (ButtonPreference) findPreference("complete_button"); preference.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { finishConfigure(); } }); } private void finishConfigure() { Log.d(TAG, "#finishConfigure"); // Make sure we pass back the original appWidgetId Intent resultValue = new Intent(); resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mAppWidgetId); setResult(RESULT_OK, resultValue); finish(); } }
- #onCreate
起動時のIntentより、APPWIDGET_IDを取得しています。
複数のwidgetを配置した場合に、識別するためのキーとなります。
- #finishConfigure
SettingActivityに配置したボタンを押下した際に呼ばれるよう、onCreate で指定しました。
http://y-anz-m.blogspot.jp/2011/06/androidappwidget.html
を参考に、widgetのidを入れ、RESULT_OKを指定しました。
Pocket (Formerly Read It Later) のOAuthログインサンプルを作りました
全てのソースコードは下記より
https://github.com/noboru-i/ReadItNow/tree/pocket-login-sample
下記のように取得できると思います。
git clone https://github.com/noboru-i/ReadItNow.git cd ReadItNow git checkout -b pocket-login-sample refs/tags/pocket-login-sample
取得後、
/res/values/common_strings.xml
を
<string name="pocket_api_key" translatable="false">10633-60f45438c2b0e5f6a98387fe</string>
のようにAPIキーを設定します。
基本的に、
http://getpocket.com/developer/docs/authentication
を実装しただけです。
APIキーの取得
http://getpocket.com/developer/
より、「CREATE NEW APP」を押下。
- Application Name
アプリケーション名を指定します。
http://getpocket.com/developer/docs/branding にあるように、「Pocket」・「Read It Later」は利用できないようです。
- Application Description
説明文を指定します。
認証画面で表示されます。
- Permissions
参照だけであれば、「Retrieve」だけでよいと思います。
- Platforms
Androidアプリであれば、「Android - Mobile」だけでよいと思います。
上記を入力し、「CREATE APPLICATION」をクリックすると、CONSUMER KEYが表示されます。
更新時には下記を追加で指定できます。
- URL
play storeのURLなどでよいかと思います。紹介ページがあればそちらを。
- Application Icon
アイコンを指定します。
認証画面などで表示されます。
- Categories
カテゴリを指定します。
http://getpocket.com/apps/
に表示される際に利用されるようです。
認証を解除する
ブラウザより
http://getpocket.com/connected_accounts
にアクセスし、「Remove access」をタップ
処理フロー
まず、MainActivity#onCreateが呼び出されます。
ランチャーから起動された場合、intent.getAction()には"android.intent.action.MAIN"が入っていることになります。
なので、36行目のif文の中には入らずに72行目のtaskを起動します。
getRequestTokenで、request tokenを取得し、preferenceに保存します。
startOauthActivityで、外部ブラウザを起動し、認証してもらいます。
認証から戻ってきた場合、36行目のif文に入ります。
preferenceよりrequest tokenを取得し、43行目のtaskを起動します。
getAccessTokenで、access tokenを取得する。
/src/hm/orz/chaos114/android/readitnow/MainActivity.java
https://github.com/noboru-i/ReadItNow/blob/pocket-login-sample/src/hm/orz/chaos114/android/readitnow/MainActivity.java
package hm.orz.chaos114.android.readitnow; import java.io.IOException; import java.io.UnsupportedEncodingException; import org.apache.http.HttpResponse; import org.apache.http.client.methods.HttpPost; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.protocol.HTTP; import org.apache.http.util.EntityUtils; import org.json.JSONObject; import android.app.Activity; import android.content.Intent; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; import android.preference.PreferenceManager; import android.util.Log; import android.view.Menu; import android.widget.TextView; import android.widget.Toast; public class MainActivity extends Activity { private static final String TAG = MainActivity.class.getSimpleName(); @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Intent intent = getIntent(); if (Intent.ACTION_VIEW.equals(intent.getAction())) { Uri data = intent.getData(); if (data != null && "authorizationFinished".equals(data.getEncodedAuthority())) { SharedPreferences preferences = PreferenceManager .getDefaultSharedPreferences(getApplicationContext()); final String requestToken = preferences.getString( "REQUEST_TOKEN", null); AsyncTask<Void, Void, Boolean> task = new AsyncTask<Void, Void, Boolean>() { @Override protected Boolean doInBackground(Void... params) { try { getAccessToken(requestToken); } catch (Exception e) { return false; } return true; } @Override protected void onPostExecute(Boolean result) { if (!result) { // 認証失敗 Toast.makeText(MainActivity.this, "認証に失敗しました", Toast.LENGTH_LONG).show(); return; } SharedPreferences preferences = PreferenceManager .getDefaultSharedPreferences(getApplicationContext()); String username = preferences.getString("USERNAME", null); TextView view = (TextView)MainActivity.this.findViewById(R.id.main_test); view.setText("Hello " + username); } }; task.execute((Void) null); return; } } AsyncTask<Void, Void, Void> task = new AsyncTask<Void, Void, Void>() { @Override protected Void doInBackground(Void... params) { // request tokenを取得する String requestToken = getRequestToken(); SharedPreferences preferences = PreferenceManager .getDefaultSharedPreferences(getApplicationContext()); Editor editor = preferences.edit(); editor.putString("REQUEST_TOKEN", requestToken); editor.commit(); // 認証画面を表示する startOauthActivity(requestToken); return null; } }; task.execute((Void) null); } private String getRequestToken() { String url = getString(R.string.url_v3_request); JSONObject param = new JSONObject(); try { param.put("consumer_key", getString(R.string.pocket_api_key)); param.put("redirect_uri", "readitnow://authorizationFinished"); String response = postJson(url, param); JSONObject respJson = new JSONObject(response); String requestToken = respJson.getString("code"); return requestToken; } catch (Exception e) { throw new RuntimeException(e); } } private void startOauthActivity(String requestToken) { String url = getString(R.string.url_authrize); Uri.Builder builder = new Uri.Builder(); builder.appendQueryParameter("request_token", requestToken); builder.appendQueryParameter("redirect_uri", "readitnow://authorizationFinished"); String queryString = builder.build().toString(); url = getString(R.string.url_authrize); Intent intent = new Intent(); intent.setAction(Intent.ACTION_VIEW); intent.setData(Uri.parse(url + queryString)); startActivity(intent); } private void getAccessToken(String requestToken) { String url = getString(R.string.url_v3_authrize); JSONObject param = new JSONObject(); try { param.put("consumer_key", getString(R.string.pocket_api_key)); param.put("code", requestToken); String response = postJson(url, param); JSONObject respJson = new JSONObject(response); String accessToken = respJson.getString("access_token"); String username = respJson.getString("username"); SharedPreferences preferences = PreferenceManager .getDefaultSharedPreferences(getApplicationContext()); Editor editor = preferences.edit(); editor.putString("USERNAME", username); editor.putString("ACCESS_TOKEN", accessToken); editor.commit(); Log.d(TAG, "accessToken = " + accessToken); Log.d(TAG, "username = " + username); } catch (Exception e) { throw new RuntimeException(e); } } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.activity_main, menu); return true; } /** * APサーバにPOSTリクエストを発行する。 * * @param endpoint リクエストURL * @param params リクエストパラメータ * @return レスポンス文字列 * @throws IOException 通信例外 */ private static String postJson(final String endpoint, final JSONObject params) throws IOException { Log.i(TAG, "endpoint = " + endpoint); DefaultHttpClient httpClient = new DefaultHttpClient(); HttpPost httpPost = new HttpPost(endpoint); try { httpPost.setHeader(HTTP.CONTENT_TYPE, "application/json"); httpPost.setHeader("X-Accept", "application/json"); StringEntity se = new StringEntity(params.toString()); httpPost.setEntity(se); } catch (UnsupportedEncodingException e1) { e1.printStackTrace(); } HttpResponse response = httpClient.execute(httpPost); String responseString = EntityUtils.toString(response.getEntity()); Log.i(TAG, "response = " + responseString); int statusCode = response.getStatusLine().getStatusCode(); if (statusCode != 200) { throw new IOException("Post failed. statusCode=" + statusCode); } return responseString; } }
/res/values/url_strings.xml
https://github.com/noboru-i/ReadItNow/blob/pocket-login-sample/res/values/url_strings.xml
<resources> <string name="url_v3_request" translatable="false">https://getpocket.com/v3/oauth/request</string> <string name="url_authrize" translatable="false">https://getpocket.com/auth/authorize</string> <string name="url_v3_authrize" translatable="false">https://getpocket.com/v3/oauth/authorize</string> </resources>
TetherSettingをリリースしました
10/24にTetherSettingをリリースしました。
https://play.google.com/store/apps/details?id=hm.orz.chaos114.android.tethersetting
テザリング時のSSIDとパスワードを変更できます。
1-Click WiFi Tether No Root
https://play.google.com/store/apps/details?id=com.diyphonegadgets.wifihotspot
Quick Settings
https://play.google.com/store/apps/details?id=com.bwx.bequick
など、テザリングのON/OFFのみが可能なソフトは多数ありますが、パスワードの設定はアプリ内では行えませんでした。
104SHでは、テザリングが解禁されていないため、テザリング設定画面は「問題が発生したため、設定を終了します。」となってしまいました。
このアプリでは、SSID・パスワードの設定もアプリ内で行えます。
そのため、104SHでもパスワード付きのテザリングが出来るようになります。
このアプリは104SHでしか、動作テストを行なっていません。
動作した端末・動作しなかった端末などあれば、レビューに記載をお願いします。
文字列を暗号化し、文字列として保存する
・暗号化キーは端末内で作成、Preferenceで保存する
・暗号化した文字列をPreferenceで保存する
・暗号化はAESを利用する
ということを実現してみました。
PreferenceUtilは、SharedPreferencesに保存するヘルパークラスです。
package hm.orz.chaos114.android.tumekyouen.util; import java.security.GeneralSecurityException; import java.security.Key; import java.security.SecureRandom; import javax.crypto.Cipher; import javax.crypto.KeyGenerator; import javax.crypto.spec.SecretKeySpec; import android.content.Context; import android.util.Base64; public class EncryptionUtil { /** 鍵のbit数 */ private static final int ENCRYPT_KEY_LENGTH = 128; /** 暗号化キー */ private final Key key; /** コンテキスト */ private final Context context; /** * コンストラクタ。 * 暗号化キーを生成、もしくは復元します。 * * @param context コンテキスト */ public EncryptionUtil(Context context) { this.context = context.getApplicationContext(); // Preferenceから暗号化キーを取得 PreferenceUtil preferenceUtil = new PreferenceUtil(this.context); String keyStr = preferenceUtil.getString(PreferenceUtil.KEY_SECRET_KEY); if (keyStr == null) { // Preferenceから取得できなかった場合 // 暗号化キーを生成 key = generateKey(); // 生成したキーを保存 String base64Key = Base64.encodeToString(key.getEncoded(), Base64.URL_SAFE | Base64.NO_WRAP); preferenceUtil.putString(PreferenceUtil.KEY_SECRET_KEY, base64Key); } else { // Preferenceから取得できた場合 // キーを復元 byte[] keyBytes = Base64.decode(keyStr, Base64.URL_SAFE | Base64.NO_WRAP); key = new SecretKeySpec(keyBytes, "AES"); } } /** * 暗号化した文字列を返却する。 * * @param input 入力文字列 * @return 暗号化した文字列 */ public String encrypt(String input) { if (input == null) { return null; } try { Cipher cipher = Cipher.getInstance("AES"); cipher.init(Cipher.ENCRYPT_MODE, key); byte[] result = cipher.doFinal(input.getBytes()); return Base64.encodeToString(result, Base64.URL_SAFE | Base64.NO_WRAP); } catch (GeneralSecurityException e) { throw new RuntimeException(e); } } /** * 復号化した文字列を返却する。 * * @param input 入力文字列 * @return 復号化した文字列 */ public String decrypt(String input) { if (input == null) { return null; } try { Cipher cipher = Cipher.getInstance("AES"); cipher.init(Cipher.DECRYPT_MODE, key); byte[] result = cipher.doFinal(Base64.decode(input, Base64.URL_SAFE | Base64.NO_WRAP)); return new String(result); } catch (GeneralSecurityException e) { throw new RuntimeException(e); } } /** * 暗号化キーを生成する。 * * @return 暗号化キー */ private static Key generateKey() { try { KeyGenerator generator = KeyGenerator.getInstance("AES"); SecureRandom random = SecureRandom.getInstance("SHA1PRNG"); generator.init(ENCRYPT_KEY_LENGTH, random); return generator.generateKey(); } catch (Exception e) { throw new RuntimeException(e); } } }
※2015/08/06追記
Android Studioの警告から知りましたが、`Cipher.getInstance`の引数は「動作モード」と「パディング」も指定しないといけないようです。
AES暗号アルゴリズムを使用してデータを暗号化する « Tech Booster
ここを読むと、`AES/CBC/PKCS5Padding`ってのが良さそうです。
ただし、この場合"IV(Initialization Vector)"なるものを保存しないといけないようです。
Lint実行時に「is not translated in」エラーより除外する
サーバのURLなんかの、多言語化する必要の無いものもvaluesフォルダのstring.xmlに定義していたところ、
Lint実行時に
"server_url" is not translated in ja
とエラーになってしまった。
Lintのエラーを警告に落とすことも出来るみたいだけど、警告が残るのもイヤ。
指定したものだけエラーの対象外(多言語化の必要がない)としたい。
http://stackoverflow.com/questions/11443996/lint-how-to-ignore-key-is-not-translated-in-language-errors
に答えが書いてあった。
<string name="server_url" translatable="false">http://...</string>
ログ出力用のTAGを簡単に定義する
クラス定義の度に、下記を書くのは面倒。
private static final String TAG = HogeActivity.class.getSimpleName();
かといって、Logを一切出力しないクラスもあり、
http://visible-true.blogspot.jp/2010/12/androidutillogtag.html
の追記のように、生成時に出力されても、警告(unused)が発生する。
なので、テンプレートとして定義することにしました。
Eclipseの設定より、
Java->Editor->Templates
へ移動し、Newボタンを押下
ダイアログにて
Name: tag Pattern: private static final String TAG = ${enclosing_type}.class.getSimpleName();
として保存。
これにより、必要になったタイミングで「tag」と入力し、コンテンツアシストで自動的に出力される。
eclipseにて、coffeescriptを記述するための準備
Help -> Install New Software...
にて、下記のURLを追加。
http://coffeescript-editor.eclipselabs.org.codespot.com/hg/
下記のエラーが発生。
Cannot complete the install because one or more required items could not be found. Software being installed: Coffeescript Runtime and UI Features 0.2.2.201203211152 (csep.feature.feature.group 0.2.2.201203211152) Missing requirement: csep 0.2.2 (csep 0.2.2) requires 'bundle org.eclipse.xtext.xtend2.lib 2.0.0' but it could not be found Cannot satisfy dependency: From: Coffeescript Runtime and UI Features 0.2.2.201203211152 (csep.feature.feature.group 0.2.2.201203211152) To: csep [0.2.2]
http://www.eclipse.org/Xtext/download.html
から、
http://download.eclipse.org/modeling/tmf/xtext/updates/composite/releases/
をコピーし、
Help -> Install New Software...
にてURLを追加。
大量に出てきたので、とりあえずキャンセル。
もう一度、coffeescript-editorをインストールしようとすると、成功。
ただし、なにかエラーが出ている・・・。
Jenkins ユーザ・カンファレンス 2012 東京まとめ
Jenkinsプロジェクト現状報告とこれから(さったホール)
世界中で利用されている
開発も活発(本体もプラグインも)
週1でのリリース
長期安定版(Long-Term Support Release)もある
BuildHiveが便利そう
GitHubとJenkinsの連携
今後の展望
プラグイン開発者を助ける
REST APIの改善などなど
スイートスポットの拡大
小さいプロジェクトから大きなプロジェクトまで適用可能に
増え続けるプラグインへの対応
Amazonのように、レコメンド機能
開発言語に合わせたセットの作成
SIerのJenkins事情(S406)
Jenkinsで実施すること
- 自動ビルド
- 静的コード解析
- 自動テスト
- カバレッジ
- 自動デプロイ
- 規模計測
目論見
- 品質向上
- コスト削減
- プロジェクト状況の見える化
取り組み
SDワークベンチという社内ブランドとして展開
開発環境ツールセットにJenkinsを追加した
事例A
10年間の長期運用
1.2Mstep
Java
ウォータフォール
月一のリリース
・既に運用が進んでいるため、影響が少ないところから始める
デプロイは従来通り手作業
テスト自動化・コードの静的解析をJenkinsにて実行
・動作環境が古いと、ジョブごとにミドルウェアの変更が必要な場合がある
・ビジネスへのインパクトが大きい機能を自動テスト
・品質管理チームが開発LANから切り離されていると、レポートのポータビリティが重要
・お客様の理解が得られたので、今後はデプロイなども自動化していく
事例K
3〜4ヶ月
120人
250kstep
Java
ウォーターフォール
・Maven+Nexusで共有
ライブラリの共有・管理
・デプロイ先が20面、1面に対して1日4,5回、担当者2名
自動化しないと対応できない
事例B
2年10ヶ月
1200人
4Mstep
Java
ウォータフォール
・最初は計画していなかった
それぞれのチームでビルドスクリプトなどは作成されていた
・結合テストに入り、内部リリースの頻度が増大した
対象マシンも40台位上
頻度・数が多く、自動化が必要
1つの環境にデプロイし、30分動作が確認できれば全環境にデプロイ
・各チームのビルドスクリプトをJenkinsでまとめて管理
・貸出返却は意味が無い
1000人規模で防げるエラーが2件だけだった
→撤廃
まとめ
ビルド・テスト環境へのデプロイで活用された
テスト環境面が多数存在した場合に効果を発揮
メトリクス自動取得・テストの自動化から導入を始めた
既存プロセスがある場合はそこから始めるのがよい
ポータビリティがある結果レポートが必要
SIの現状に合うプラクティスを取り入れていけば、開発スピードはまだまだ上がる
目的にあわせてプロジェクトに入れやすい形を模索していこう
愛されるJenkins氏になるために(S406)
ゲームで使用されている技術と課題
技術
- サーバ
- クライアント
- JavaScript
- ngCore
課題
超高速リリース
iOS特有の制限によるスケジュールの縛り
歴史的経緯のソースコード
全サポート端末で安定性の確保
複数ブランチ(ゲームイベント毎ぐらいで作られる)の並行開発
マスターブランチに自動テストを流す
→あんまり意味ない
git-flowによる解決策
→無理でした
全ブランチに対して自動テストを実行
iPadをXFDに(全ブランチの結果を一覧化)
→赤くなったら修正する!
落ちるテストの問題
200程度のテストで落ちていたが、一旦無効化
→グリーンに保つことが大事(エラーケースはDBスキーマ依存などだった)
既存コードは諦める
新規ソースはがんばる
ベストプラクティスは守っておいたほうがいいよね、という認識への変化
今後やっていきたいこと
・自動テストの並列実行
・クライアントサイドの自動テスト
マルチステージ型継続的インテグレーションのすすめ(S406)
テクマトリックス株式会社
マルチステージ型=複数箇所で継続的インテグレーションを行う
複数のチームがある場合、それぞれでCIを回し、結合する際には同程度の成熟度にする。
商用製品を使っての説明だったので、個人的にあんまり興味を持てなかった。
毎日が憧れの新築、反復可能なデリバリーによる常時新築システム(さったホール)
http://www.slideshare.net/ohtaketomohiro/ss-13793834
富士通研究所
環境構築は1回やって終わりのことが多い
その後は手順書を修正しながらだったり、修正されなかったり・・・
PaaSの環境構築の時の話
元々はデプロイに8人日(+アルファ)かかっていた
愚直に自動化
環境構築も自動化しよう
Chef Soloを利用
・パッケージの集約
アーキテクチャ毎にビルドサーバを用意しビルド
その後、集約ジョブを実行
・マシンの用意
Java, Git, Ruby, Chef-soloをインストールしたテンプレートVMを用意
テンプレートからVMを作り、そこにデプロイ
電源投入などもジョブとして実行
・デプロイの実行
ノードをまたがって逐次実行
master -> powercli -> slave
slaveは複数台
スローデプロイ問題
自動化してもデプロイに60分かかる
スローテスト問題は、テストケースを分割して並列実行が1つの解
→デプロイは並列実行ができない
・VMスナップショットへの復元
10分→3分
制約は増えるが速度は改善
・インターネットからの分離
rpmやgemのダウンロードを内部化
45分→2分
インターネット接続が無い環境へのデプロイが可能に
・自前と既成の分割と冪等性
冪等性:f(f(x)) = f(x)
・自前(自分のソースからビルド)
・既成(他人が作ったもの)
更新頻度・インストール所要時間に違いがある
→デプロイスクリプトを自前・既成に分割
→冪等(何度実行しても同じ)なデプロイスクリプトを作成する
自動化と高速化による変化
・早い失敗を目指す
テストでのエラーより、デプロイ時のエラー
設定ファイルではなく、設定ファイル作成スクリプト
→設定値の入力チェックなどを行える
・環境の使い捨て
本番と同じ環境で開発
・ブランチごとにデプロイ
毎回新規に作られる安心感
まとめ
自動化できるものは自動化
・デリバリーも自動化しましょう
・クリーンと冪等で反復可能に
自動化をより高速に
・高速化の検討すべき
・迅速なフィードバック
・速ければ使い方も変わる
執事のJenkinsさんは万能
・CI以外もこなせる
・執事を遣い倒そう
LT大会:書籍執筆における継続的デリバリー(さったホール)
Kinect本執筆時の話
SPHINX
Pythonで動く
テキストベース(バージョン管理しやすい)
PDFにはビルドが必要
→Jenkinsを利用
どこで動かす?
CloudBees
githubへpush→CloudBeesでePubへ→編集者が確認
。。。時間終了
LT大会:英語Jenkinsコミュニティの解説(さったホール)
(逐次通訳だったので聞くのに忙しかった)
結論:英語コミュニティにぜひ参加してください
LT大会:Flash(ActionScript3.0)開発でもJenkinsを導入しよう(さったホール)
antがあれば何でもできる(猪木風)
→でも大変。
Maven3で
JenkinsサーバにFlash Playerをインストール
Xが無いと動作しない
xvfb(仮想ディスプレイ)上でFlash Playerを実行
問題点
64bitOS上で、Flash自体の動作が不安定
実行時間が長い
。。。時間終了
LT大会:普通のSIerのJenkinsのある暮らし(さったホール)
社内フレームワーク開発の話
サポートする環境の組み合わせが多い
(JDKは1.5〜7、DBも複数製品・複数バージョン)
自動デプロイ
Antを実行
複数環境で自動テストを行うことで、バージョン組み合わせのバグを検出できる
自動化することで精神的に安定する
。。。無事終了
LT大会:運用でも使えるJenkins(さったホール)
Jenkinsは開発でのみ使うものではない
Jenkinsはなんでもできる
定期バッチ
定時でのDBダンプ
定時でのファイルバックアップ
→全部Jenkinsでできる
エラーメールの送信
結果レポート
実行履歴管理
→全部Jenkinsでできる
運用でもJenkinsを使おう!
感想
まとめ書くのに疲れた。。。